Niixer

Desarrollo del Videojuego “SpeedAndJump” en Unity – Superación de Retos hasta el Castillo Final

Este documento describe el proceso de desarrollo del videojuego SpeedAndJump, un proyecto en Unity diseñado en perspectiva de primera persona. El juego consiste en guiar a un personaje a través de diversos retos, plataformas móviles, enemigos y trampas, con el objetivo final de alcanzar un castillo. Durante el desarrollo se implementaron sistemas de vida, detección de caídas, animaciones controladas por condiciones, inteligencia artificial básica en enemigos (como abejas voladoras), y una interfaz de usuario que refleja el estado del jugador. Además, se incluyeron menús interactivos con música y video, así como escenas de Game Over y Victoria. El documento incluye también los scripts utilizados, explicados con su funcionalidad y propósito, brindando una visión completa del diseño técnico y jugable del proyecto.

figura 1

Importación de Assets y Configuración Inicial del Personaje

Todos los modelos 3D y animaciones utilizados en el juego fueron importados desde el paquete Ultimate Platformer Pack de Quaternius, descargado desde el sitio oficial de Unity Asset Store. El paquete fue incorporado al proyecto arrastrando su carpeta directamente al panel Project de Unity, permitiendo así su reconocimiento automático por el motor. Desde esta ubicación se seleccionaron los modelos, animaciones y objetos físicos requeridos para el desarrollo del entorno y la mecánica del juego.

figura 1

figura 2

figura 2

figura 3

figura 4

Se llevaron a cabo los siguientes pasos clave para la configuración inicial del personaje principal y los elementos físicos del entorno:

  • Importación del Modelo del Jugador: El modelo 3D fue importado en formato .FBX desde el paquete Ultimate Platformer Pack. En la pestaña Rig del Inspector, se configuró el tipo de animación como Humanoid para facilitar la integración con el sistema de animación de Unity.
  • Sistema de Animaciones: Las animaciones correspondientes fueron arrastradas a un Animator Controller, donde se definieron los estados y transiciones. Se configuraron parámetros como isRunning (booleano) para controlar el movimiento, y Die (Trigger) para gestionar la animación de muerte. Se desactivó la opción Has Exit Time en la transición hacia la animación de muerte para permitir una respuesta inmediata ante eventos de colisión letal.
  • Física del Entorno: A los elementos del entorno (como plataformas, obstáculos y superficies) se les asignaron componentes Rigidbody (cuando se requería dinámica) y Collider (BoxCollider, MeshCollider o CapsuleCollider, según el objeto), con el fin de gestionar las colisiones y simular interacciones físicas realistas.
  • Control del Personaje: Se utilizó el componente CharacterController para el movimiento del jugador, acompañado de scripts personalizados que gestionan la lógica de salto, detección de caída, y condiciones de muerte (por ejemplo, caída desde cierta altura o colisión con elementos hostiles).

figura 8

Sistema de Vida y Detección de Caídas

Se implementó un sistema de vidas que determina cuántas veces el jugador puede caer desde una altura peligrosa antes de activar el estado de Game Over. La lógica se basa en dos condiciones principales:

figura 9

Detección de Caída por Altura: Si el jugador cae desde una altura superior al valor definido como minFallHeight, se considera una caída peligrosa y se decrementa el contador de vidas disponibles.

figura 10

Límite de Posición Vertical: Independientemente de la altura desde la que se cayó, si la posición vertical del jugador (transform.position.y) desciende por debajo de un umbral crítico definido (por ejemplo, deathYThreshold), se ejecuta automáticamente la pérdida de una vida. Esta condición asegura que caídas prolongadas fuera del escenario también sean detectadas como fallos

figura 11

Sistemas de animación

Las animaciones del personaje fueron gestionadas a través de un Animator Controller, que definió el flujo de estados como:

Idle – cuando el personaje está detenido.

Walk / Run – durante el movimiento a baja o alta velocidad.

Die – animación de muerte activada tras una caída o evento letal.

figura 12

Para activar la animación de muerte, se definió un parámetro de tipo Trigger llamado Die, el cual es invocado desde el script del controlador de vida en el momento en que el jugador pierde una vida.

Con el fin de asegurar que la animación de muerte se reproduzca completamente antes de que el personaje reaparezca, se utilizó una corrutina (Coroutine) que introduce un retardo basado en la duración exacta de la animación. Solo después de ese intervalo se restablece la posición del jugador al punto de inicio o respawn.

figura 13

Interfaz de Usuario (UI) y Control de Daño

El sistema de interfaz de usuario fue diseñado para reflejar en tiempo real el estado de vidas del jugador. Para ello, se implementó un script llamado UIManager, encargado de actualizar dinámicamente los elementos visuales (como íconos de vidas o contadores numéricos) cada vez que se produce una pérdida de vida.

figura 14

Creación e Integración del Enemigo: Abeja

Importación del Modelo 3D

El modelo de la abeja fue importado en formato .FBX y ubicado en la ruta: Assets/Enemies/Bee.

En el panel Inspector, se configuró el tipo de Rig como Humanoid o Generic, dependiendo del formato del asset original y la compatibilidad de las animaciones.

Configuración del Prefab

  • Se generó un Prefab denominado Bee arrastrando el modelo desde el proyecto a la carpeta Assets/Prefabs.
  • Se añadieron los siguientes componentes:
    • Animator: con el BeeAnimatorController asignado.
    • Rigidbody: configurado con:
      • Mass = 1
      • Drag = 0.5
      • Use Gravity = false (para simular vuelo continuo).
    • SphereCollider: utilizado para detectar colisiones físicas. Se mantuvo Is Trigger = false para permitir interacción física con el jugador.
  • El objeto fue etiquetado con:
    • Tag = Enemy
    • Layer = Enemies
      Esto facilita la identificación mediante scripts y la gestión de colisiones con capas específicas.

figura 15

Configuración del Animator Controller

Se configuró el BeeAnimatorController con los siguientes parámetros y estados:

  • Parámetros:
    • isFlying (bool): activado por defecto para mantener el estado de vuelo.
    • Attack (Trigger): utilizado para iniciar la animación de ataque cuando se detecta al jugador.
  • Estados:
    • BeeFlying: clip de animación de vuelo, configurado como Default State (flecha naranja), con la opción Loop Time activada.
    • Attack: animación de ataque activada mediante trigger.
  • Transiciones:
    • Any State → Attack: condición Attack (Trigger activado).
    • Any State → BeeFlying: condición isFlying == true.
  • Se desactivaron Has Exit Time y Write Defaults en transiciones específicas para asegurar una respuesta inmediata y optimizar el control sobre los parámetros del Animator.

figura 16

Script BeeMovement.cs — Movimiento y Ataque del Enemigo Abeja

El script BeeMovement.cs controla el comportamiento de patrullaje, persecución y ataque del enemigo tipo abeja. Sus principales componentes funcionales son:

Inicialización (Start())

void Start() {
animator = GetComponent<Animator>();
animator.SetBool("isFlying", true); // Estado inicial de vuelo
player = GameObject.FindGameObjectWithTag("Player").transform; // Referencia al jugador
}

Actualización de Comportamiento (Update())

En cada frame, se evalúa la distancia al jugador y se ejecutan distintas acciones:

void Update() {
float distanceToPlayer = Vector3.Distance(transform.position, player.position);

if (distanceToPlayer <= followRange) {
// Movimiento hacia el jugador
transform.position = Vector3.MoveTowards(transform.position, player.position, speed * Time.deltaTime);
animator.SetBool("isFlying", true);

// Ataque si está en rango y respetando el cooldown
if (distanceToPlayer <= attackRange && Time.time - lastAttackTime >= attackCooldown) {
animator.SetTrigger("Attack");
lastAttackTime = Time.time;
}
} else {
// Patrullaje flotante (Idle)
FloatIdle();
}
}
  • followRange: distancia máxima para comenzar a seguir al jugador.
  • attackRange: distancia mínima para activar el ataque.
  • lastAttackTime y attackCooldown: variables para controlar la frecuencia de ataque y evitar ataques continuos.

figura 17

Método de Movimiento Flotante (Idle)

void FloatIdle() {
float yOffset = Mathf.Sin(Time.time * floatSpeed) * floatAmplitude;
transform.position = new Vector3(transform.position.x, initialY + yOffset, transform.position.z);
}
  • Simula un movimiento vertical suave con Mathf.Sin para dar sensación de vuelo o patrullaje sin jugador cerca.
  • Se recomienda almacenar el valor initialY en Start() para conservar el nivel base del eje Y.

figura 18

Evento de Ataque: DealDamage()

public void DealDamage() {
// Este método se llama desde un evento en el clip de animación 'Attack'
if (Vector3.Distance(transform.position, player.position) <= attackRange) {
player.GetComponent<PlayerHealth>().TakeDamage(damageAmount);
}
}
  • Este método se conecta directamente desde el evento de animación del clip de ataque.
  • Asegura que el daño se inflija solo si el jugador está dentro del rango al momento del impacto.
  • Llama al método TakeDamage() del componente PlayerHealth para reducir vidas

figura 19

Implementación de Menú Principal con Canvas

Creación de Escena MainMenu

  • Se creó una nueva escena MainMenu.unity.
  • Se añadió a Build Settings para garantizar su correcta carga como primera escena del juego.

figura 20

Configuración del Canvas

  • Se añadió un Canvas al escenario.
  • Render ModeScreen Space - Overlay para una interfaz nítida y adaptativa en cualquier resolución.
  • Se ajustaron dimensiones y anclajes para una experiencia responsiva.

figura 21

Elementos de UI

  • Botón “Play”:
    • Tipo: Button (TextMeshPro).
    • Evento OnClick() asignado a MainMenuController.PlayGame(), encargado de cargar la siguiente escena de juego.
  • Texto descriptivo:
    • Añadido al Canvas.
    • Tipografía elegante con TextMeshPro, tamaño y color ajustados para legibilidad y estética.
    • Centrado y estilizado para acompañar el diseño general del menú.

figura 22

Fondo de Video

  • Se creó la carpeta Assets/Videos e importó un archivo .mp4.
  • Se añadió un componente VideoPlayer al Canvas:
    • Render ModeCamera Near Plane (se renderiza detrás de los elementos UI).
    • Target CameraMainCamera.
    • Loop: activado.
    • Play On Awake: activado.
    • Audio Output ModeNone (audio silenciado para no interferir con la música de fondo).
  • Se ajustó el orden de render para mantener los elementos UI por encima del video.

figura 23

Música de Fondo

  • Se importó MenuMusic.mp3 en la carpeta Assets/Audio.
  • Se creó un GameObject llamado MusicPlayer con componente AudioSource:
    • AudioClip: asignado a MenuMusic.mp3.
    • Play On Awake: activado.
    • Loop: activado.
    • Volumen: ajustado a 0.3 para no opacar efectos futuros del juego.

figura 24

Anexo Completo de Scripts con Código y Descripción

AnimationEventRelay.cs.

Sirve como conexión entre una acción animada y la forma en que el jugador ataca.Captura un Evento de Animación y redirige la solicitud al método de ataque del PlayerController. Coordina las animaciones con la implementación del daño, garantizando precisión en el tiempo. con tecla E

using UnityEngine;

public class AnimationEventRelay : MonoBehaviour
{
    public PlayerController playerController;

    // Este método será llamado por el evento de animación
    public void PerformAttack()
    {
        if (playerController != null)
        {
            playerController.PerformAttack();
        }
    }
}

BeeMovement.cs

Regula las acciones de la abeja rival: flotación sin movimiento, seguimiento al jugador, animación de ataque y asistencia de daño.Administra los estados de inactividad en el aire y la persecución/ataque según las distancias.Presenta un enemigo que se mueve de forma dinámica y pone a prueba al jugador.

using UnityEngine;

public class BeeMovement : MonoBehaviour
{
    public float floatSpeed = 2f;
    public float floatAmount = 0.2f;
    public float followRange = 6f;
    public float attackRange = 1.2f;
    public float moveSpeed = 3f;
    public float attackCooldown = 1.0f;

    private Vector3 startPosition;
    private Transform player;
    private Animator animator;
    private float lastAttackTime = -Mathf.Infinity;

    void Start()
    {
        startPosition = transform.position;
        animator = GetComponent<Animator>();
        GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
        if (playerObj != null)
            player = playerObj.transform;
    }

    void Update()
    {
        if (player == null)
        {
            FloatIdle();
            return;
        }

        float distance = Vector3.Distance(transform.position, player.position);

        if (distance <= followRange)
        {
            // Seguir al jugador
            Vector3 targetPos = player.position;
            targetPos.y = transform.position.y;
            transform.position = Vector3.MoveTowards(transform.position, targetPos, moveSpeed * Time.deltaTime);

            // Atacar si está en rango
            if (distance <= attackRange)
            {
                if (Time.time - lastAttackTime >= attackCooldown)
                {
                    if (animator != null)
                        animator.SetTrigger("Attack");
                    lastAttackTime = Time.time;
                }
            }
        }
        else
        {
            FloatIdle();
        }
    }

    void FloatIdle()
    {
        float newY = startPosition.y + Mathf.Sin(Time.time * floatSpeed) * floatAmount;
        transform.position = new Vector3(transform.position.x, newY, transform.position.z);
    }

    // Este método debe ser llamado por un evento en la animación de ataque
    public void DealDamage()
    {
        if (player == null) return;
        float distance = Vector3.Distance(transform.position, player.position);
        if (distance <= attackRange)
        {
            PlayerLifeSystem pls = player.GetComponent<PlayerLifeSystem>();
            if (pls != null && pls.CurrentLives > 0)
                pls.TakeDamage();
        }
    }
}

CameraFollow.cs

Establece una cámara en tercera persona que se maneje con el ratón y que tenga un seguimiento fluido. Modifica el movimiento vertical y horizontal con el ratón, y haz que la posición y la vista hacia el jugador sean más suaves. Brinda al jugador un control visual total y una experiencia inmersiva.

using UnityEngine;
using UnityEngine.InputSystem;

public class CameraFollow : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 3, -6);
    public float followSpeed = 10f;
    public float mouseSensitivity = 2f;
    public float minY = -35f;
    public float maxY = 60f;

    private float currentYaw = 0f;
    private float currentPitch = 10f;

    void LateUpdate()
    {
        if (!enabled || target == null) return;

        float mouseX = Mouse.current.delta.x.ReadValue() * mouseSensitivity;
        float mouseY = Mouse.current.delta.y.ReadValue() * mouseSensitivity;

        currentYaw += mouseX;
        currentPitch -= mouseY;
        currentPitch = Mathf.Clamp(currentPitch, minY, maxY);

        target.rotation = Quaternion.Euler(0, currentYaw, 0);

        Quaternion rotation = Quaternion.Euler(currentPitch, currentYaw, 0);
        Vector3 desiredPosition = target.position + rotation * offset;

        transform.position = Vector3.Lerp(transform.position, desiredPosition, followSpeed * Time.deltaTime);
        transform.LookAt(target.position + Vector3.up * offset.y * 0.5f);
    }
}

CoinSpawner.cs

Crea monedas de vida sobre cada dificultad.Al comenzar, identifica elementos con la etiqueta ‘Obstáculo’ y crea monedas. Automatiza las recogidas, incentivando la exploración.

using UnityEngine;

public class CoinSpawner : MonoBehaviour
{
    public GameObject coinPrefab; // Asigna el prefab desde el editor
    public float coinOffsetY = 1.0f; // Altura sobre el cubo

    void Start()
    {
        GameObject[] obstacles = GameObject.FindGameObjectsWithTag("Obstacle");
        foreach (GameObject obstacle in obstacles)
        {
            Vector3 spawnPosition = obstacle.transform.position + new Vector3(0, coinOffsetY, 0);
            Instantiate(coinPrefab, spawnPosition, Quaternion.identity);
        }
    }
}

Enemy.cs

Clase fundamental para oponentes con puntos de vida y fin de existencia.Reduce los puntos de vida y elimina el objeto cuando llegan a cero.Estructura consistente para todos los oponentes, permite una fácil ampliación.

using UnityEngine;

public class Enemy : MonoBehaviour, IDamageable
{
    public int maxHealth = 3;
    private int currentHealth;

    void Start()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int amount)
    {
        currentHealth -= amount;
        if (currentHealth <= 0)
        {
            Die();
        }
    }

    void Die()
    {
        Destroy(gameObject);
    }
}

FallingPlatform.cs

Plataforma que cae al pisarla y resetea su estado. Retraso, caída con gravedad extra y reinicio.

using UnityEngine;

public class FallingPlatform : MonoBehaviour
{
    public float fallDelay = 0.5f;
    public float extraGravityMultiplier = 3f;

    private Rigidbody rb;
    private bool hasFallen = false;
    private bool isInactive = false;
    private Vector3 initialPosition;
    private Quaternion initialRotation;
    private Collider platformCollider;
    private Renderer platformRenderer;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        platformCollider = GetComponent<Collider>();
        platformRenderer = GetComponent<Renderer>();
        initialPosition = transform.position;
        initialRotation = transform.rotation;
        rb.isKinematic = true;
    }

    public void TriggerFall()
    {
        if (!hasFallen && !isInactive)
        {
            hasFallen = true;
            Invoke(nameof(Fall), fallDelay);
        }
    }

    void Fall()
    {
        rb.isKinematic = false;
        rb.velocity = Vector3.down * Physics.gravity.magnitude * extraGravityMultiplier;
    }

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("MainPlatform") && !isInactive)
        {
            isInactive = true;
            platformCollider.enabled = false;
            platformRenderer.enabled = false;
            rb.isKinematic = true;
        }
    }

    public void ResetPlatform()
    {
        isInactive = false;
        transform.position = initialPosition;
        transform.rotation = initialRotation;
        rb.isKinematic = true;
        hasFallen = false;
        if (platformCollider != null) platformCollider.enabled = true;
        if (platformRenderer != null) platformRenderer.enabled = true;
    }
}

GameOverUI.cs

Controlador del menú Game Over.Reinicia o cierra el juego y permite al jugador reintentarlo o salir.

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameOverUI : MonoBehaviour
{
    public void Retry()
    {
        SceneManager.LoadScene("SampleScene");
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

GreenCoin.cs

RMoneda que otorga vida al jugador. Comprueba si necesita vida y la otorga.

using UnityEngine;

public class GreenCoin : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        PlayerLifeSystem playerLife = other.GetComponent<PlayerLifeSystem>();
        if (playerLife != null)
        {
            if (playerLife.NeedsLife())
            {
                playerLife.GainLife();
                Destroy(gameObject);
            }
            else
            {
                Debug.Log("Vidas completas, no se puede recoger la moneda.");
            }
        }
    }
}

KillZone.cs

Zona letal que daña al jugador. OnTriggerEnter quita una vida y define zonas de peligro en el nivel.

using UnityEngine;

public class KillZone : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag(“Player”))
        {
            PlayerLifeSystem pls = other.GetComponent<PlayerLifeSystem>();
            if (pls != null)
            {
                pls.TakeDamage();
            }
        }
    }
}

MainMenu.cs

Maneja botones del menú principal.Inicia, muestra opciones y cierra con navegación inicial del juego.

using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenu : MonoBehaviour
{
    public void StartGame()
    {
        SceneManager.LoadScene("YourGameScene");
    }

    public void Options()
    {
        Debug.Log("Opciones");
    }

    public void QuitGame()
    {
        Application.Quit();
        Debug.Log("Juego cerrado");
    }
}

MainMenuManager.cs

Controla inicialización y transición del menú al juego.Pausa, oculta UI y reanuda con Play.Transición fluida entre UI y gameplay.

using UnityEngine;

public class MainMenuManager : MonoBehaviour
{
    public CameraFollow cameraFollowScript;
    public GameObject Canvas;

    void Start()
    {
        Time.timeScale = 0;
        Cursor.lockState = CursorLockMode.None;
        Cursor.visible = true;
        cameraFollowScript.enabled = false;
        Canvas.SetActive(false);
    }

    public void PlayGame()
    {
        Time.timeScale = 1;
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        cameraFollowScript.enabled = true;
        Canvas.SetActive(true);
        gameObject.SetActive(false);
    }
}

MovingPlatform.cs

Plataforma que se mueve entre dos puntos,Kinematic MoveBetween A-B con velocidad definida. Desafío de sincronización y dinamismo al nivel.

using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class MovingPlatform : MonoBehaviour
{
    public Vector3 pointA = Vector3.zero;
    public Vector3 pointB = new Vector3(3, 0, 0);
    public float speed = 2f;
    public float tolerance = 0.05f;

    private Vector3 worldPointA, worldPointB, target, velocity;
    private Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.isKinematic = true;
        rb.interpolation = RigidbodyInterpolation.Interpolate;
        worldPointA = transform.position + pointA;
        worldPointB = transform.position + pointB;
        target = worldPointB;
    }

    private void FixedUpdate()
    {
        Vector3 current = rb.position;
        Vector3 newPos = Vector3.MoveTowards(current, target, speed * Time.fixedDeltaTime);
        velocity = (newPos - current) / Time.fixedDeltaTime;
        rb.MovePosition(newPos);
        if (Vector3.Distance(newPos, target) < tolerance)
            target = target == worldPointA ? worldPointB : worldPointA;
    }

    public Vector3 GetVelocity() => velocity;

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        Vector3 a = Application.isPlaying ? worldPointA : transform.position + pointA;
        Vector3 b = Application.isPlaying ? worldPointB : transform.position + pointB;
        Gizmos.color = Color.green; Gizmos.DrawSphere(a, 0.1f);
        Gizmos.color = Color.red;   Gizmos.DrawSphere(b, 0.1f);
        Gizmos.color = Color.yellow;Gizmos.DrawLine(a, b);
    }
#endif
}

PlayerController.cs

Control principal de jugador: movimiento, salto, ataque y plataforma. Input System, animaciones, detección OverlapSphere y TriggerFall.

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    public float speed = 5f; public float gravity = -9.81f; public float jumpHeight = 2f; public float speedSmooth = 10f;
    public float attackRange = 1.5f; public int attackDamage = 1; public LayerMask enemyLayer;

    private CharacterController controller; private PlayerControls inputActions;
    private Vector2 moveInput; private Vector3 velocity; private bool jumpRequested = false;
    private Animator animator; private float currentAnimSpeed = 0f;

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
        inputActions = new PlayerControls();
        animator = GetComponentInChildren<Animator>();
    }

    private void OnEnable()
    {
        inputActions.Player.Enable();
        inputActions.Player.Move.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
        inputActions.Player.Move.canceled += ctx => moveInput = Vector2.zero;
        inputActions.Player.Jump.performed += ctx => jumpRequested = true;
        inputActions.Player.Attack.performed += ctx => animator.SetTrigger("Attack");
    }

    public void PerformAttack()
    {
        Collider[] hits = Physics.OverlapSphere(transform.position, attackRange, enemyLayer);
        foreach (Collider c in hits)
            c.GetComponent<IDamageable>()?.TakeDamage(attackDamage);
    }

    private void Update()
    {
        bool isGrounded = controller.isGrounded;
        animator.SetBool("isGrounded", isGrounded);
        float targetSpeed = new Vector2(moveInput.x, moveInput.y).magnitude;
        currentAnimSpeed = Mathf.Lerp(currentAnimSpeed, targetSpeed, speedSmooth * Time.deltaTime);
        animator.SetFloat("Speed", currentAnimSpeed);
        animator.SetBool("isRunning", targetSpeed > 0.1f);

        if (isGrounded)
        {
            if (velocity.y < 0) velocity.y = -2f;
            if (jumpRequested)
            {
                velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
                jumpRequested = false;
                animator.ResetTrigger("Jump"); animator.SetTrigger("Jump");
            }
        }
        velocity.y += gravity * Time.deltaTime;
        Vector3 move = transform.right * moveInput.x + transform.forward * moveInput.y;
        controller.Move((move * speed + Vector3.up * velocity.y) * Time.deltaTime);
    }

    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        hit.gameObject.GetComponent<FallingPlatform>()?.TriggerFall();
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
    }
}

PlayerLifeSystem.cs

Sistema de puntos de vida del jugador con respawn y Game Over.Caídas, caída fuera del mapa, UI de vidas, reinicio plataformas. Control del ciclo de vida del jugador y gestión de errores en caída.

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class PlayerLifeSystem : MonoBehaviour
{
    public int maxLives = 3; public Transform respawnPoint; public float minFallHeight = 2.357f; public float deathYLimit = -20f;
    public UIManager uiManager;

    private int currentLives; private float highestY; private bool isFalling = false; private bool wasGroundedLastFrame = true; private bool lifeLostThisFall = false;
    private CharacterController controller; private GameObject lastGroundedObject;
    public Vector3 velocity = Vector3.zero;
    public int CurrentLives => currentLives;

    void Start()
    {
        controller = GetComponent<CharacterController>();
        if (controller == null || respawnPoint == null) { enabled = false; return; }
        currentLives = maxLives; highestY = transform.position.y;
        uiManager?.UpdateLives(currentLives);
    }

    void Update()
    {
        bool isGrounded = controller.isGrounded; float currentY = transform.position.y;
        if (!isGrounded)
        {
            if (currentY > highestY) highestY = currentY;
            if (!isFalling) { isFalling = true; lifeLostThisFall = false; }
        }
        if (isGrounded && !wasGroundedLastFrame && isFalling)
        {
            float fallDistance = highestY - currentY;
            RaycastHit hit; if (Physics.Raycast(transform.position, Vector3.down, out hit, 2f)) lastGroundedObject = hit.collider.gameObject;
            if (fallDistance >= minFallHeight && !lifeLostThisFall && lastGroundedObject.CompareTag("MainPlatform"))
            { LoseLife(); lifeLostThisFall = true; }
            ResetFallTracking();
        }
        if (transform.position.y < deathYLimit && !lifeLostThisFall) { LoseLife(); lifeLostThisFall = true; }
        wasGroundedLastFrame = isGrounded;
    }

    void OnControllerColliderHit(ControllerColliderHit hit) { if (controller.isGrounded) lastGroundedObject = hit.gameObject; }

    void LoseLife()
    {
        currentLives--; uiManager?.UpdateLives(currentLives);
        if (currentLives > 0) Respawn(); else GameOver();
    }
    void Respawn()
    {
        velocity = Vector3.zero; controller.enabled = false; transform.position = respawnPoint.position; controller.enabled = true; ResetFallTracking();
        foreach (var p in FindObjectsByType<FallingPlatform>(FindObjectsSortMode.None)) p.ResetPlatform();
    }
    void GameOver() { SceneManager.LoadScene("GameOverScene"); }
    void ResetFallTracking() { highestY = transform.position.y; isFalling = false; }
    public bool NeedsLife() => currentLives < maxLives;
    public void GainLife() { if (NeedsLife()) { currentLives++; uiManager?.UpdateLives(currentLives); } }
    public void TakeDamage() { LoseLife(); }
}

Roller.cs

Rodillo que daña al jugador y se destruye al finalizar recorrido.Detecta triggers y colisiones con el Player.

using UnityEngine;

public class Roller : MonoBehaviour
{
    public bool HasLeftPlatform { get; private set; } = false;
    public bool IsDone { get; private set; } = false;
    public bool HasTouchedPlazaRoll { get; private set; } = false;

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("PlazaRoll"))
            HasTouchedPlazaRoll = true;
        else if (other.CompareTag("RollerEnd"))
        {
            HasLeftPlatform = true;
            IsDone = true;
            Destroy(gameObject);
        }
    }

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            collision.gameObject.GetComponent<PlayerLifeSystem>()?.TakeDamage();
        }
    }
}

RollerSpawner.cs

Generador alternado de rodillos en puntos izquierdo/derecho. Controla spawn y evita repeticiones.Ritmo de obstáculos y variabilidad.

using UnityEngine;
using System.Collections;

public class RollerSpawner : MonoBehaviour
{
    public GameObject[] rollerPrefabs;
    public Transform leftSpawnPoint;
    public Transform rightSpawnPoint;
    public float spawnDelay = 1.5f;

    private bool spawnLeftNext = true;
    private int lastPrefabIndex = -1;

    void Start()
    {
        StartCoroutine(SpawnRollersAlternately());
    }

    IEnumerator SpawnRollersAlternately()
    {
        while (true)
        {
            Transform sp = spawnLeftNext ? leftSpawnPoint : rightSpawnPoint;
            int idx = GetRandomPrefabIndex();
            GameObject roller = Instantiate(rollerPrefabs[idx], sp.position, Quaternion.identity);
            Roller rs = roller.GetComponent<Roller>();
            if (rs != null)
                yield return new WaitUntil(() => rs.HasTouchedPlazaRoll);
            else
                yield return new WaitForSeconds(spawnDelay);
            spawnLeftNext = !spawnLeftNext;
        }
    }

    int GetRandomPrefabIndex()
    {
        int idx;
        do { idx = Random.Range(0, rollerPrefabs.Length); }
        while (idx == lastPrefabIndex);
        lastPrefabIndex = idx;
        return idx;
    }
}

UIManager.cs

Gestiona UI de vidas.Actualiza contador en pantalla.Feedback inmediato al jugador sobre sus vidas restantes.

using UnityEngine;
using TMPro;

public class UIManager : MonoBehaviour
{
    public TextMeshProUGUI livesText;

    public void UpdateLives(int currentLives)
    {
        livesText.text = "Vidas: " + currentLives;
    }
}

VictoryTrigger.cs

Detecta llegada a meta y activa UI de victoria

using UnityEngine;

public class VictoryTrigger : MonoBehaviour
{
    public VictoryUIManager victoryUI;

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            victoryUI.ShowVictoryScreen();
        }
    }
}

VictoryUIManager.cs

Controla panel de victoria y navegación entre niveles.

using UnityEngine;
using UnityEngine.SceneManagement;

public class VictoryUIManager : MonoBehaviour
{
    public GameObject victoryPanel;

    void Start()
    {
        if (victoryPanel != null)
            victoryPanel.SetActive(false);
    }

    public void ShowVictoryScreen()
    {
        if (victoryPanel != null)
        {
            Time.timeScale = 0f;
            victoryPanel.SetActive(true);
        }
    }

    public void RetryLevel()
    {
        Time.timeScale = 1f;
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }

    public void NextLevel()
    {
        Time.timeScale = 1f;
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if (nextSceneIndex < SceneManager.sceneCountInBuildSettings)
            SceneManager.LoadScene(nextSceneIndex);
        else
            Debug.Log("¡No hay más niveles!");
    }
}


Video explicación

¿Cómo se juega SpeedAndJump?

🎯 Objetivo principal:

Sobrevivir y avanzar por un entorno de plataformas mientras esquivas peligros, derrotas enemigos (como abejas), recoges monedas de vida y llegas al final del nivel (zona de victoria).


🕹️ Controles del jugador:

Movimiento: Con teclado (probablemente con WASD o flechas).

Cámara: Controlada con el ratón (rotación horizontal y vertical).

Saltar: Presionar una tecla (probablemente Espacio) para realizar un salto.

Atacar: Usar una tecla (probablemente Click izquierdo o Ctrl) para ejecutar una animación de ataque que daña enemigos cercanos.


❤️ Sistema de vidas:

  • Empiezas con 3 vidas.
  • Pierdes una vida si:
    • Caes desde una altura muy alta.
    • Te caes del mapa.
    • Te ataca un enemigo.
    • Entras en una zona letal (KillZone).
  • Puedes recuperar vidas recogiendo monedas verdes (GreenCoin), pero solo si no tienes el máximo.

🐝 Enemigos:

Hay abejas voladoras que siguen al jugador y atacan cuando están cerca.

Al recibir daño, el jugador pierde una vida.

Las abejas se comportan de forma dinámica: siguen, flotan y atacan.


⚠️ Obstáculos y plataformas:

Hay plataformas móviles y otras que caen al pisarlas, aumentando el desafío.

Rodillos que se mueven y pueden dañar al jugador (Roller).

Zona letal (KillZone) que mata automáticamente al jugador si entra.


🧩 Recursos y recolección:

Monedas de vida sobre los obstáculos. Aparecen automáticamente al inicio (CoinSpawner).

Sistema de UI que te muestra cuántas vidas te quedan.


🏁 Fin del juego:

Ganas si llegas al final del nivel y activas la zona de victoria (VictoryTrigger).

Pierdes si se te acaban las vidas y se carga la escena de Game Over (GameOverScene).

Video Juego SpeedAndJump

https://youtube.com/watch?v=j7Lq0_ZDTHE%3Ffeature%3Doembed

Créditos

Autor: Diana Marcela Sanchez Sanchez

Editor: Carlos Iván Pinzón Romero

Código: UCMV-9 – Modelado 3D y Videojuegos

UniversidadUniversidad central

Referencia

Unity Technologies. (2025). Software de desarrollo de juegos: Crea juegos 2D y 3D. Unity. https://unity.com/es/games


Unity Technologies. (s.f.). Guía de inicio rápido para el desarrollo de juegos 3D. Recuperado el 23 de mayo de 2025, de https://docs.unity3d.com/6000.1/Documentation/Manual/Quickstart3D.html


Unity Technologies. (2025). Unity Asset Store: The Best Assets for Game Making. Unity. https://assetstore.unity.com/


Pixabay. (s.f.). Efectos de sonido de bosque horror - búsqueda. https://pixabay.com/es/sound-effects/search/bosque-horror/


Unity Technologies. (2022). Tiling Textures - 3D Microgame Add-Ons. AssetStore Unity. https://assetstore.unity.com/packages/2d/textures-materials/tiling-textures-3d-microgame-add-ons-174461