Curso de Unity 3D

  1. Introducción
  2. Instalación
  3. Interfaz
  4. Conceptos básicos
  5. Ejercicio: cubos
  6. Cambiar el color de algo
  7. Introducción a C#
  8. Conceptos para realización de juegos 2D
  9. Acceder a componentes desde código
  10. Uso del transform
  11. User Interface
  12. Elije tu propia aventura
  13. Acierta imagen
  14. Sprites
  15. Fall Down Game
  16. Poner una imagen de fondo
  17. Instanciar
  18. Galería de tiro
  19. Corrutinas
  20. Flappy Bird
  21. PONG
  22. Carreras
  23. Panel Animator
  24. Plataformas
  25. Plataformas
  26. Puzzle con Raycast
  27. Plataformas con RayCast
  28. Memory (juego de las parejas)
  29. Guardar y recuperar información almacenada
  30. Publicación
  31. Ejercicio: First Person Shooter
  32. Multijugador con Photon

Introducción

¿Qué es un motor de videojuegos?

Algunos ejemplos de motores de videojuegos son gamesalad, gamemaker, construct (no tiene versión para mac), Stencyl (tiene versión nativa para Linux), Unreal Engine

Nos permiten exportar para diversas plataformas: Nintendo, PlayStation, PC, Mac, web, Linux..., dependiendo del motor.

Características

  1. Soporta desarrollo de videojuegos en 2 y 3 dimensiones.
  2. Soporta varios lenguajes de programación: C#, Javascript
  3. Ejemplos de juegos hechos con Unity
  4. Pese a ser muy potente tiene una interfaz sencilla y un desarrollo mediante código bastante legible.
  5. Nos permite publicar el juego sin ningún tipo de coste

Diferencias entre la versión personal y la PRO

Instalación

Tiene dos versiones: Personal y Profesional. La versión Personal carece de ciertas funcionalidades de la Profesional. Muchas de estas son para trabajo en equipo.

En linux: Descarga

En windows y mac: Enlace

Nuevo proyecto

El nombre que le asignemos será el que utilice Unity 3D para almacenar en nuestro disco todos los ficheros de nuestro proyecto.

Definimos si es un proyecto 3D o 2D. Esto se puede cambiar una vez iniciado el proyecto (menu edit -> Project settings -> Editor -> Mode: 2D).

También podemos añadir assets (fotos, audios, etc) a nuestro proyecto, lo cual aumentará su peso. Esto lo podremos hacer durante la realización del proyecto.

En itch.io puedes descargar múltiples recursos para tus videojuegos.

Abrir código con monodevelop

  1. Instalamos monodevelop. En Linux:
    sudo apt-get install monodevelop
  2. Edit -> Preferences -> External Tools -> External Script Editor -> /usr/bin/monodevelop (en Linux)

Interfaz

Barra de herramientas horizontal superior

desplegables unity 3d

Paneles

Se pueden redimensionar arrastrando desde los bordes de los paneles.

Se pueden arrastrar y soltar. Puedo restaurar la distribución de paneles anterior, seleccionando un layout en el último desplegable de la barra de herramientas.

Si hemos cerrado un panel, se puede volver a traer de vuelta a través del menú window.

Panel de jerarquía

Contiene un listado de todos los jerarquico de todos los elementos que participan en la escena del juego.

Panel de escena

Contiene una representación gráfica de todos los elementos de la escena

Panel de juego (game)

Nos muestra como se vería nuestro videojuego desde el punto de vista del jugador.

En la configuración por defecto de Unity, o vemos el panel de escena o vemos el panel de juego. Es interesante ver ambos paneles a la vez. Para ello, arrastramos y soltamos el panel de juego a la derecha del panel de proyecto.

Panel inspector

Nos detalla todas las propiedades del objeto que hemos seleccionado.

Si selecciono el candado de este panel, seguirán activas las propiedades del objeto seleccionado aunque seleccione otro objeto.

Panel de proyecto

Es el equivalente a un explorador de ficheros para visualizar los ficheros de nuestro proyecto. Me permite marcar ciertos objetos como favoritos.

Haciendo click derecho en el panel puedo crear nuevas entidades.

Manteniendo ctrl pulsado y moviendo la ruedita del ratón puedo aumentar o reducir el tamaño de los items.

Un asset es cualquier tipo de arvhivo: video, audio, foto, etc.

Un package es un conjunto de assets.

Cuando importamos un asset, lo que ocurre internamente es que se crea una copia en la carpeta de assets.

Botón derecho - import new asset: nos permite importar uno y sólo un fichero.

Botón derecho -> Open containg folder: Abre la carpeta de assets utilizando el explorador de ficheros del sistema. Luego puedo pegar en esta carpeta los ficheros necesarios y aparecen en el panel de proyecto de Unity 3D.

Panel de consola

Nos muestra los errores de nuestros scripts.

Conceptos básicos

Escenas

Son ficheros con extensión .unity

Son los niveles del juego, las pantallas del menú, etc.

Para guardar escenas crearemos una carpeta en el panel de proyecto y ahí será dónde las guardemos.

Game Objects

Dentro de una escena hay "Game objects", que son los objetos que puedes ver en el panel de jerarquía. Puede ser una foto, un video, un script, etc.

Podemos añadir Game Objects haciendo uso de su correspondiente menú.

Componentes

Son las propiedades de un Game Object.

Podemos ver los componentes de un Game Object en el panel inspector

Un componente que tienen todos los Game Objects es el componente Transform.

Desde el menú Components puedo añadir componentes a un Game Object.

float x = GetComponent<Transform> ().position.x;
...
transform.position = new Vector2 (pos_center,0);

Moviendonos en el espacio 3D

Orbitar: alt + click + movimiento del ratón. Debes estar en vista 3D.

Desplazar en un eje: seleccionamos el objeto. Seleccionamos y arrastramos un eje. Si en el componente transform de un Game Object selecciono la ruedita y le doy a reset, el objeto se posicionará en el punto 0,0,0.

Span view: pulso q, y a partir de ahí puedo desplazarme haciendo click con el ratón y moviéndolo.

Desplazar un eje, Rotar, escalar, escalar sólo en una dirección: Al seleccionar un Game Object, vemos que en la barra de herramientas horizontal superior hay varias posibilidades para seleccionar que operación de transformación queremos hacer. Teniendo seleccionado un Game Object puedo acceder a estas operaciones pulsando w, e, r y t, respectivamente.

herramientas unity 3d

zoom: ctrl + ruedita del ratón.

La operaciones de transformación de un objeto se hacen a partir de su manejador, que estará siempre en el centro del objeto. Esto es algo que no podemos cambiar. Podemos ver el manejador de un objeto al hacer click sobre él.

Hacer zoom a un objeto: selecciono un objeto y pulso la f.

Duplicar: Selecciono un objeto - ctrl+c, ctrl + v.

Puedo establecer jerarquías entre los Game Object arrastrando y soltando unos sobre otros dentro del panel de jerarquías.

Cámara

Si la seleccionamos, podemos cambiar el degradado del cielo desde el panel inspector, accediendo al componente Camera -> clear flags.

Desde este componente también podremos cambiar la perspectiva. El valor 60 es más o menos lo que vemos nosotros con los ojos.

Al final del inspector, en Target display, eliminamos GUI Layer (sólo sirve para mantener compatibilidad con antiguas versiones de Unity y consume recursos), y Flare Layer.

Ejercicio: cubos

Duplicando, escalando y desplazando cubos 3D realizar una mesa. Agrupar los Game Object en el panel de jerarquías, y luego duplicar varias veces la mesa creada y agrupada.

mesas en unity3d

Cambiar el color de algo

Debo crear un nuevo material en el panel de proyecto y arrastrarlo y soltarlo sobre el objeto.

Si no quiero que el material se vea afectado por la luz, tras aplicar el material a un objeto, seleccionaré el correspondiente componente -> Shader -> Unlit -> Color

Introducción a C#

Panel de proyecto -> Create -> Script de C# -> Asocio el script a un nuevo Empty llamado "EscenaManagement".

Editor

Si estamos usando windows, lo normal es usar el visual studio. Pero si usamos linux, podemos usar el mono develop. Para vincular el monodevelop a unity3d usaremos: Edit -> Preferences -> External Tools -> monodevelop

Hola Mundo

public class HolaMundo : MonoBehaviour {
	
	void Start () {
		Debug.Log ("Hola Mundo");
	}

	void Update () {
		Debug.Log ("Adios Mundo");
	}
}

Variables

Declaración

public class Variables : MonoBehaviour {

	string textoInicio = "Hola Mundo!";

	void Start () {
		Debug.Log (textoInicio);
	}
	

	void Update () {

	}
}

Tipos de variables

public class TiposVariables : MonoBehaviour {
	public int numeroEntero = 1; // 1 2 3 4 5 6 7 8 ...
	public float numeroDecimal = 1.2f; //1.1f  1.2f  1.33f  ...
	public string texto = "El texto"; // "Van siempre entre comillas"
	public bool variableBooleana;


	// variables típicas de Unity
	public GameObject cubos;
	public Transform miTransform;
	public MeshFilter meshFilter;
	public BoxCollider boxCollider;
	public MeshRenderer meshRender;

	void Start () {

	}

	void Update () {

	}
}

Si arrastro este script a un GameObject, podré modificar las variables seleccionando el GameObject desde el panel Inspector.

Concatenaciones

public class Concatenaciones : MonoBehaviour {

	string v1 = "Buenos días ";
	string v2 = "Pablo";

	void Start () {
		Debug.Log (v1 + v2);
	}
}

Operaciones aritméticas

public class OperacionesAritmeticas : MonoBehaviour {

	int a = 6;
	int b = 8;

	void Start () {
		Debug.Log (a + b + "suma");
		Debug.Log (a - b + "resta");
		Debug.Log (a / b + "division");
		Debug.Log (a * b + "multiplicacion");
		Debug.Log (a % b + "resto");
		Debug.Log (++a + "incremento");
		Debug.Log (--a + "decremento");
	}

}

Condicionales

public class Condicionales : MonoBehaviour {

	public int velocidadCoche = 0;

	void Start () {

	}

	void Update () {
		if(velocidadCoche > 40){
			Debug.Log ("Vas muy rápido");	
		}else if(velocidadCoche < 38){
			Debug.Log ("Vas muy lento");
		}else{
			Debug.Log (velocidadCoche);
		}
		velocidadCoche++;
	}

/*
a > b
a < b
a >= b
a <= b
a == b
a != b
a > b && a > 3
a > b || a > 3
*/
}

Arrays

No es posible cambiar dinámicamente su tamaño.

public class Arrays : MonoBehaviour {

	string [] letras = new string[]{"a", "b", "c", "d", "e"};

	void Start () {
		Debug.Log (letras [2]);
	}
}

Obtener un nuevo array con una posición eliminada

public static class Tools{
	public static T[] GetNewArrayRemovingAt<T>(this T[] source, int index){
		T[] dest = new T[source.Length - 1];
		if (index > 0)
			Array.Copy(source, 0, dest, 0, index);

		if (index < source.Length - 1)
			Array.Copy(source, index + 1, dest, index, source.Length - index - 1);
		return dest;
	}
}
miArray = miArray.GetNewArrayRemovingAt(indice);

Listas

Es posible cambiar dinámicamente su tamaño.

Ejemplo de creación de una lista de enterosList<int> numeros = new List<int>(new int[]{ 1,2,3});
Eliminamos el segundo elemento de la listanumeros.RemoveAt(2);

Bucle for

public class Bucles : MonoBehaviour {

	public int [] numeros = new int[6]{0, 1, 2, 3, 4, 5};

	void Start () {
		for (int i = 0; i < numeros.Length; i++) {
			Debug.Log (numeros[i]);
		} 
	}
}

Bucle foreach

public class BucleForeach : MonoBehaviour {

	string[] animales = { "perro", "gato", "elefante" };

	void Start () {
		foreach(string animal in animales){
			Debug.Log(animal);
		}	
	}
}

Bucle doWhile

public class BucleDoWhile : MonoBehaviour {

	string[] animales = { "perro", "gato", "elefante" };

	void Start () {
		int i = 0;
		do{
			Debug.Log(animales[i]);
			i++;
		}while(i < animales.Length );		
	}	
}

Switch Case

public class SwitchCase : MonoBehaviour {
	int caso = 1;

	void Start () {
		switch(caso){
			case 0:
				Debug.Log("aaaaaaaaa");
			break;
			case 1:
				Debug.Log ("bbbbbbbbb");
			break;
			default:
			break;
		}
	}
}

Funciones

public class Funciones : MonoBehaviour {

	void Start () {
		saludar ();
	}

	void saludar(){
		Debug.Log ("hola");
	}
}

Función con parámetros

public class FuncionConParametros : MonoBehaviour {

	void Start () {
		saludar ("Juan");
	}

	void saludar (string nombre) {
		Debug.Log ("hola " + nombre);
	}
}

Función con parámetros y return

public class FuncionConReturn : MonoBehaviour {

	void Start () {
		string saludo = saludar ("Juan");
		Debug.Log (saludo);
	}

	string saludar (string nombre) {
		string txt = "hola " + nombre;
		return txt;
	}
}

Programación orientada a objetos

public class Coche{
	public string marca;
	public int cantidadRuedas;

	public void encenderMotor (){
		Debug.Log ("El motor está encendido");
	}
	public void apagarMotor(){
		Debug.Log ("El motor está apagado");
	}
}
public class Carretera : MonoBehaviour {

	Coche miCoche;

	void Start(){
		crearCoche ();
	}
	void crearCoche(){
		miCoche = new Coche ();
		miCoche.marca = "Ford";
		miCoche.cantidadRuedas = 4;
		miCoche.encenderMotor ();
		Debug.Log ("La marca de mi coche es " + miCoche.marca);
		Debug.Log ("Mi coche tiene " + miCoche.cantidadRuedas + " ruedas");
		apagarCoches ();
	}

	void apagarCoches(){
		miCoche.apagarMotor ();
	}
}

Las clases que heredan de MonoBehaviour nos permiten sobrecargar el método update.

Conceptos para realización de juegos 2D

Para cambiar entre vista 3D y 2D: Edit -> Proyect Settings -> Editor -> Default behaviour mode

Las luces no tienen efecto sobre los sprites, pero sí sobre las texturas.

Para que los objetos 3D se vean con colores nítidos, igual que ocurre con los sprites: Window -> Rendering -> lighting Settings -> Scene -> Enviroment Lighting:

Para ubicar los objetos en 2D, procuraremos usar siempre z = 0.

Para modificar un sprite, usaremos habitualmente la tecla t, para entrar en el modo rect tool. Si mantenemos shift presionado, deformaremos el objeto proporcionalmente. Si mantenemos alt presionado, lo haremos desde el centro del objeto.

Inspector de Sprites

Advanced

Filter mode: Bilinear -> eso hace que los bordes del sprites sean nítidos, en lugar de emborronarse al juntarse con la escena.

Tamaño de los assets

Unity proceará mucho mejor los assets cuyo tamaño corresponde a una potencia de de 2. En el siguiente ejemplo usamos un asset 2048x2048.

Al seleccionar un Asset en Unity, en el panel inspector, podremos ver un previo de la foto junto con su tamaño y la palabra NPOT, que significa que cumple la regla de que el asset tiene un tamaño que es potencia de 2.

Acceder a componentes desde código

Acceder a componentes del propio objeto

public class Codigo : MonoBehaviour{
	private Transform tr;

	void Start(){
		//GetComponent es un método del objeto implícito gameObject
		tr = GetComponent<Transform>();

		// Es posible acceder directamente al componente transform de un objeto de la siguiente forma:
		tr = transform;

		Debug.Log(tr.position.x);
	}
}
Descargar ejemplo acceso a componentes

Acceder a componentes de otro objeto

public class Codigo : MonoBehaviour
{
    private Transform tr;

    void Start()
    {
        GameObject go = GameObject.Find("Cube");
        Transform tr = go.transform;
        Debug.Log(tr.position.x);
    }
}

El método Find sólo accede al primer nivel de hijos de un elemento. Si queremos acceder recursivamente a los sucesivos niveles de hijos de un lemento podemos usar una función recursiva.

public static Transform RecursiveFindChild(this Transform padre, string nombreABuscar){

        for (int i = 0; i < padre.childCount; i++){
			if (padre.GetChild(i).name == nombreABuscar){
                return padre.GetChild(i);
            }else if (padre.childCount > 0){
                Transform hijoEncontrado = RecursiveFindChildAux(padre.GetChild(i), nombreABuscar);

                if (hijoEncontrado != null){
                    return hijoEncontrado;
                }
            }
        }
        Debug.LogError("No se encuentra este objeto como hijo: " + nombreABuscar);
        return null;
    }

    static Transform RecursiveFindChildAux(this Transform padre, string nombreABuscar){

        for (int i = 0; i < padre.childCount; i++){
            if (padre.GetChild(i).name == nombreABuscar){
                return padre.GetChild(i);
            }else if (padre.childCount > 0){
                Transform hijoEncontrado = RecursiveFindChildAux(padre.GetChild(i), nombreABuscar);

                if (hijoEncontrado != null){
                    return hijoEncontrado;
                }
            }
        }
        return null;
    }
Ejemplo de usoobjeto.transform.RecursiveFindChild("numero")
Descargar ejemplo acceso a componentes 2

Uso del transform

void Update () {
	miTransform.Translate (Vector3.right * velocidad * Time.deltaTime);
	miTransform.Rotate (Vector3.up * velocidadRotacion * Time.deltaTime);
}
Descargar ejemplo de uso de transform

User Interface

  1. Creamos un nuevo proyecto 2D.
  2. Panel jerarquía -> botón derecho -> UI -> Button. Cada vez que añadimos un UI a escena estará anidado bajo un único GameObject Canvas principal.

Componentes del canvas

Canvas

En el panel de jerarquías definiremos que elementos de la UI se verán por encima (lo que esté más arriba se verá por encima).

Rect Transform

Es un componente de todos los objetos UI.

El punto de anclaje de un rect transform (una cruceta compuesta de cuatro aspas independientes) determina respecto a qué puntos va a estar ubicado.

Puedo separar las aspas del punto de anclaje para definir el tamaño porcentual de la pieza respecto del canvas.

Pulsando en el icono cuadrado que se muestra en el panel de jerarquías puedo reubicar automáticamente los puntos de anclaje.

Propiedad Render Mode:

Canvas Scaller

Propiedad UI Scale Mode, que define como se van a comportar los componentes que hay dentro del canvas:

Captura de eventos

Al añadir un canvas a la pantalla, se crea un GameObject en la jerarquía llamado EventSystem, que es el que captura los eventos.

Ejercicios Canvas

  1. Botones

    Poner cuatro botones en pantalla, ocupando cada uno un cuarto de la misma. Simplemente crearemos un botón, lo modificaremos para que ocupe un cuarto de la pantalla y lo duplicaremos hasta obtener los cuatro botones.

    Tener en cuenta que si queremos lograr el efecto snap en un elemento de canvas, debemos modificarlo arrastrando sus esquinas, en lugar de simplemente desplazarlo.

    El botón será verde al pasar el cursor por encima y rojo al ser pulsado.

    botones en la user interface de unity3d Descargar ejercicio botones
  2. Ejercicio: UI Paneles

    Los paneles sirven para definir grupos de Game Objects. Es posible definir el tamaño de lo que contienen desde el propio panel.

    1. Crear un panel con cuatro botones dentro y probar a asignarle diferentes tipos de layouts. Un layout es un componente:
      • Horizontal Layout
      • Vertical Layout
      • Grid Layout
      . Ajustar las opciones del layout para que los botones ocupen el 100% del mismo.
    2. Tras haber realizado el paso anterior, probar a añadir más botones el layout y ver que pasa.
    hacer layouts con la user interface de unity
  3. Ejercicio - Hacer un piano

    1. Creamos un nuevo proyecto 2D.
    2. Creamos una carpeta "sounds", e importamos en ella los sonidos del piano. sonidos del piano
    3. Creo un panel con layout horizontal. Podemos eliminarle el componente Image (no tendrá una imagen de fondo).
    4. Añado a este panel un botón al que le elimino el texto.
    5. Añado al botón un componente de tipo audio source, que vinculo con el sonido DO.
    6. Modificando el componente button del botón, hacer que al pulsar sobre él se reproduzca el sonido ( play() ) de su audioSource.
    7. Duplico 6 teclas más dentro del panel (re, mi, fa, sol, la, si) y les asigno a todas su correspondiente audio source.
    8. Añadimos un nuevo panel para los tonos sostenidos. Podemos eliminarle el componente Image (no tendrá una imagen de fondo).
    9. Añadimos los botones para los sostenidos a este último panel. Tendrán color negro. Ajustamos su posición y tamaño manualmente.
    piano con unity3d
  4. Recupera y modifica un texto

    recoger valor de la UI con Unity 3D Descargar ejercicio UI
    1. Ubicamos en pantalla un UI -> Input Field, un UI -> Button y un UI -> Text.
    2. Vamos a trabajar usando el tamaño de la cámara para el canvas. Para ello:
      1. Seleccionamos el canvas en el panel de jerarquía.
      2. Componente Canvas -> Render mode: World Space.
      3. Vamos a ajustar el canvas a la escena. Para ello:
        1. Lo desplazamos: x:0, y:0, z:0.
        2. Lo escalamos (es importante no cambiar su width y height o se verá pixelado). x:0.4, y:0.4, z:0.4.
    3. Creamos un GameObject de tipo Empty y le vinculamos el siguiente código:
      using UnityEngine;
      using UnityEngine.UI; // Si estás programando elementos del camvas, no te olvides de importar esta librería
      
      public class GameController : MonoBehaviour
      {
          public void mostrarValor()
          {
              string valor = GameObject.Find("TextoDelInput").GetComponent().text;
              GameObject.Find("Resultado").GetComponent().text = valor;
          }
      
      }
    4. Seleccionamos el Button -> Componente Button -> On Click -> Seleccionamos en la pestaña escena que se despliega al pulsar el circulito el empty con el script asociado -> Seleccionamos la función mostrarValor().
  5. Acierta número secreto

    acierta número secreto con unity 3d Descargar ejercicio acierta número secreto

    Debemos genarar un número secreto cuando se inicialice el juego. Para ello, declararemos una variable global n y la inicializaremos dentro del método Start():

    void Start(){
    	n = Random.Range(1, 3);
    	Debug.Log(n);
    }

    Debemos comparar el valor introducido por el usuario con el almacenado en la variable n. Como el número introducido por el usuario será de tipo texto, y n es de tipo numérico, pasaremos el valor de n a texto en la comparación:

    if( ""+n == valor)
  6. Hacer un puzzle usando el Canvas de Unity 3D

    Descargar puzzle con canvas
    Creamos la interfaz
    1. Creamos un canvas. Es fundamental que tenga el componente Graphic Raycaster asociado.
    2. Definimos la propiedad Render mode de su componente Canvas con valor Screen Space - Overlay.
    3. Añadimos un panel al canvas.
    4. Le añadimos un layout group de tipo grid. Una vez ordenados los objetos que contendrá, habría que desactivarlo. De lo contrario, cuando arrastremos un objeto, volverá a la posición inicial.
    5. Añadimos 8 imágenes de la UI al panel.
    puzzle con canvas
    Arrastrar piezas
    Pieza.csusing UnityEngine.EventSystems;
    public class Pieza : MonoBehaviour,IDragHandler{
    	public void OnDrag(PointerEventData eventData){
    		transform.position = Input.mousePosition;
    	}
    }
    Cuando suelto la pieza, esta vuelve a su posición original
    Pieza.csusing UnityEngine.EventSystems;
    
    public class Pieza : MonoBehaviour, IDragHandler, IEndDragHandler{
    	Vector3 startPosition;
    
    	public void OnDrag(PointerEventData eventData){
    		transform.position = Input.mousePosition;
    	}
    
    	public void OnEndDrag(PointerEventData eventData){
    		Debug.Log ("Termina");
    		transform.position = startPosition;
    	}
    }
    OnBeginDrag

    Aunque para resolver este ejercicio no sea necesario, tener en cuenta que también podemos detonar un evento mientras estamos arrastrando la pieza:

    public class Pieza : MonoBehaviour, IBeginDragHandler{
    
    public void OnBeginDrag(PointerEventData eventData){
    	Debug.Log ("Empieza");
    	startPosition = transform.position;
    }

    Desordenar piezas

    Si el panel dónde están las piezas tiene un horizontal o un vertical layout, habrá que desactivarlo para poder desordenar las piezas.

    Pieza.cstransform.position = new Vector2(Random.Range(0, Screen.width), Random.Range(0, Screen.height));

    Encajar piezas

    Habrá que desmarcar la check Raycast Target del componente Image para poder clickar de los paneles para poder clickar sobre los botones que están debajo.

    public void OnEndDrag(PointerEventData eventData){
    	Debug.Log ("suelta" + Vector3.Distance (transform.position, posIni) );
    	if (Vector3.Distance (transform.position, posIni) <= margenError) {
    		...
    	}	
    }

Elije tu propia aventura

Haremos click sobre zonas de la pantalla que nos llevarán al éxito o a la muerte.

flujo juego elige tu propia aventura

Ajustar la imagen de fondo a la pantalla

  1. Creamos un nuevo Proyecto 2D. La cámara tendrá proyección Ortográfica.
  2. Definimos la relación de aspecto correcta: Pestaña Game -> Desplegable de relación de aspecto -> 3:2
  3. Definimos un size para la main camera, de tal forma que abarque correctamente las dimensiones de la foto que vamos a insertar: Main Camera -> Size: 3.2
  4. Insertamos la foto arrastrándola y soltándola sobre la pantalla. No usaremos canvas.

Detectar la pulsación sobre un objeto

  1. Añadimos un GameObject de un cubo a la escena. Es imprescindible que para detectar la pulsación dicho GameObject tenga un BoxCollider.
  2. Vinculamos el siguiente script al GameObject:
    using UnityEngine;
    
    public class Nave : MonoBehaviour
    {
        void OnMouseDown()
        {
            Debug.Log("Nave pulsada");
        }
    }
  3. Si no queremos que se vea el GameObject añadido, podemos eliminarle su componente Mesh Renderer.

Cambiar de escena

using UnityEngine.SceneManagement;
SceneManager.LoadScene ("Muerte");

Para que funcione, las escenas deben haber sido añadidas en 'File -> Build Settings -> Scenes in Build'

Sprites

Sprites múltiples

Para optimizar el rendimiento de la aplicación, en lugar de cargar una foto por cada imagen, es recomendable cargar una foto que contenga todas las imágenes necesarias.

Todas las fotos que sean introducidas en el panel de proyecto en modo 2D son interpretadas como sprites. Si son introducidas en modo 3D son interpretadas como texturas. Una flechita al lado de la foto indica que es un sprite.

Si quiero usar sprites en un entorno 3D: selecciono la imagen -> Texture Type -> Sprite (2D and UI)

Añadir a escena un con su correspondiente animación.

Descargar sprite Tux
  1. Seleccionaremos el sprite en el panel de proyecto.
  2. Seleccionaremos Sprite Mode = multiple en el panel inspector.
  3. Pulsaremos el botón de apply en el panel inspector.
  4. Pulsaremos el botón de Sprite Editor en el panel inspector. A partir de aquí tenemos dos opciones:
    • Hacer rectángulos manualmente de los sprites que quiero seleccionar
    • Hacer un recortado automático, que en el caso de fotografías con transparencia se adaptará a los límites de la foto.
  5. A partir de este momento, podré arrastrar desde el panel de proyecto cada uno de los sprites recortados que he realizado, a la escena.

Cuando seleccionamos varios sprites y los añadimos a la escena, Unity interpreta que estos sprites forman una animación.

Ejercicio: superposición de sprites

Añadir al ejercicio anterior un segundo sprite e invertir el orden de visualización: el sprite que estaba por encima debe ubicarse por debajo.

Descargar sprite

Para gestionar esto iremos a: Panel inspector -> Sprite Renderer:

https://vimeo.com/

Cargar un sprite dinámicamente a partir de su nombre

Lo mejor es crear dentro de la carpeta Assets una carpeta llamada Resources (con R mayúscula). Ruta es una variable de tipo texto.

paneles [i].GetComponent<Image> ().sprite = Resources.Load<Sprite>(ruta-sin-poner-la-carpeta-Resources-y-sin-ponerle-la-extension-a-la-foto);

Acierta imagen

Dadas 3 imágenes alineadas horizontalmente en un panel inferior y una imagen superior centrada, si pulsamos en una imagen del panel que coincide con la superior, se disparará una traza.

imagen del juego acierta imagen

Cargamos la imagen de fondo

imagen de fondo del juego acierta imagen

Cargamos tres imágenes abajo y una posible solución arriba

unity 3d la imagen coincide

Detectar pulsación sobre las imágenes de abajo

Volvemos a repetir los pasos para escuchar la pulsación sobre un objeto en escena.

Comprobar si el sprite pulsado coincide con el sprite solución

public void OnMouseDown(){
	Sprite caraPulsada = GetComponent<SpriteRenderer>().sprite;
	Sprite caraSolucion = GameObject.Find("caraSUP").GetComponent<SpriteRenderer>().sprite;
	if(caraPulsada == caraSolucion){
		Debug.Log("coinciden");
	} else	{
		Debug.Log("no coinciden");
	}
}

Generar sprites aleatorios arriba y abajo

public class GameController : MonoBehaviour{
	public Sprite[] caras; 

	void Start(){
		nuevaPartida();
	}

	void NuevaPartida(){
		int c1 = Random.Range(0, 6);
		GameObject.Find("cara1").GetComponent<SpriteRenderer>().sprite = caras[p1];

		...

		//De momento la imágen superior coincidirá con la primera de las de abajo
		GameObject.Find("caraSUP").GetComponent<SpriteRenderer>().sprite = caras[p1];
	}
}

La solución ya no es siempre el primero, es aleatoria


void NuevaPartida(){
	...
	int[] carasPosibles = new int[3] { c1, c2, c3 };
	GameObject.Find("caraSUP").GetComponent<SpriteRenderer>().sprite = caras[carasPosibles[Random.Range(0, 3)]];
}

Las posibles soluciones no deben estar repetidas

while(c2 == c1) c2 = Random.Range (0, 6);
while(c3 == c1 || c3 == c2) c3 = Random.Range (0, 6);

Muestra un marcador

Utilizaremos un objeto 3D que nos permitirá no recurrir al canvas.

3D Object -> TextMesh Pro - Text

A veces el icono [T] del TextMeshPro molesta un poco en pantalla. Para desactivarlo, podemos ir a Desplegable Gizmos -> TeshMeshPro -> Pulsamos sobre la foto del icono [T].

Insertaremos dos objetos, uno para la palabra fija pts y otra para el marcador en si misma.

marcador con unity

Llamar a una función de una clase desde otra clase

GameController gc;

void Start(){
	gc = GameObject.Find("GameController").GetComponent<GameController>();
}

Aumentar marcador cada vez que acertamos

Definiremos una nueva variable privada que incrementaremos cada vez que el jugador acierte.

GameController.cspublic int marcador;
CaraAbajo.csGameController gc;
void Start(){
	gc = GameObject.Find("GameController").GetComponent<GameController>();
}

void OnMouseDown(){
	...
	gc.marcador++;
	GameObject.Find("marcador").GetComponent <TeshMeshPro> ().text = "" + gc.marcador;

Volver a cargar las imágenes en cada turno

Haremos esto para volver a pintar imágenes cada vez que acertamos.

Para volver a pintar las imágenes en pantalla cuando acertemos, debemos llamar desde la clase CaraAbajo a la clase GameController.

CaraAbajo.cs
...

gc.nuevaPartida();
GameController.cspublic void nuevaPartida(){
	...
}

Temporizador

El siguiente código se encarga de llamar a una función recursivamente cada segundo:

void temporizador(){
	Debug.Log("Llamada al código");
	Invoke("temporizador", 1.0f);
}

Utilizando el código anterior, decrementa el contador de tiempo.

Perder el juego

Añadir la programación necesaria para que si se agota el tiempo o el usuario no escoge la solución correcta, se lance una nueva escena indicando que el jugador ha perdido.

Fall Down Game

Ponemos el fondo de pantalla

fall down game con unity 3D

Un misil cae por gravedad

  1. Añadimos el sprite de un misil a la pantalla.
  2. Le añadimos al misil un componente de tipo RigidBody 2D.

Tipos de RigidBody

Detectamos la pulsación sobre el misil

Recuerda como lo hiciste en el juego de Elige tu propia aventura:

using UnityEngine;

public class Nave : MonoBehaviour
{
    void OnMouseDown()
    {
        Debug.Log("Nave pulsada");
    }
}

Animación de explotar

Panel animation

Muestra las animaciones vinculadas con el objeto activo.

Puedo asignar una animación a un objeto, arrastrando la animación sobre el objeto en el panel de jerarquía o sobre el inspector.

Puedo reutilizar animaciones de desplazamiento, rotación, escala y, para varios objetos. Pero las animaciones consistentes en sprites animados serán solo para un único objeto. De lo contrario, si a un pingüino le pongo una aimación de sprites de un perro andando, el pingüino se convertirá en perro.

Si no queremos que una animación se repita indefinidamente (comportamiento por defecto), seleccionaremos la animación en el panel de proyecto y desmarcaremos la check "loop" en el panel inspector.

Animar frame a frame

Pasos para hacer una animación.

  1. Debemos preparar un sprite dando los siguientes pasos en su panel inspector:
    1. Texture Type: Sprite (2D and UI)
    2. Sprite Mode: Multiple
    3. Sprite Editor
      1. SI -> Slice
        • Type: Automatic
        • Slice
  2. Seleccionamos en el panel de jerarquía el Sprite sobre el que queremos crear la animación -> Panel animation -> create new clip.
    • Creamos la animación reposo. Esta animación tiene una única que imágen que es el sprite del misil
    • Creamos la animación explosión. Esta animación tiene varios sprites, correspondientes a los estados de la explosión.
      • Habrá que ir añadiendo cada uno de estos sprites de uno en uno a la línea de tiempo del panel animation.
      • También podemos seleccionar todos los sprites de la animación (manteniendo shift pulsado mientras seleccionamos) y arrastrarlos de una sola vez a la línea de tiempo.

Por defecto Unity reproduce las animaciones en bucle. Para evitar esto, selecciono la animación creada en el panel de proyecto y desmarco la check Loop Time.

Panel Animator

Este panel gestiona la vinculación entre las animaciones y los objetos.

Si un objeto tiene errores en su panel animator, no se reproducirán las animaciones asociadas a él.

Es posible unir las animaciones vinculadas a un objeto mediante flechas. A estas uniones las llamaremos transiciones.

Estado Entry
Detonar la animación mediante código:
GetComponent<Animator>().Play("explosion");
panel animator unity 3d

Detener la caída por gravedad del objeto

GetComponent<Rigidbody2D>().simulated = false;

Destruir objeto cuando la animación ha terminado

public AnimationClip animationClip;
...
float duracionExplosion = animationClip.length;
Destroy(this.gameObject, duracionExplosion);

Generar un misil cada segundo

Usaremos un código similar al tel temporizado del juego Acierta Imagen.¿Recuerdas?

void temporizador(){
	Debug.Log("Llamada al código");
	Invoke("temporizador", 1.0f);
}

Poner una imagen de fondo

Podemos hacerlo de varias formas:

Instanciar

Instanciar un MonoBehaviour

El siguiente ejemplo instancia una imagen en pantalla ubicada dentro de la carpeta Resources.

public class SpriteGenerator : MonoBehaviour{
    public SpriteGenerator(int n){
        this.n = n;
        GameObject objetoContenedor = new GameObject("Nuevo objeto");
        SpriteRenderer spr = objetoContenedor.AddComponent();
        spr.sprite = Resources.Load("pieces/gafas");

    }
}

Instanciar un prefab

Un prefab es un GameObject que puede agrupar a su vez varios GameObjects, Scripts, assets, etc y que está empaquetado y prepararado para ser reusado.

Técnicamente, un prefab debe ser un único objeto, por lo que si quisieramos que el prefab constase de varios objetos, deberíamos agruparlos en un empty.

Para crear el prefab, lo arrastraremos del panel de jerarquía, donde lo hemos estado elaborando, hasta el panel de proyecto.

Si tengo varias instancias de un prefab y modifico una de ellas, pulsando el botón apply en el panel inspector, estas modificaciones se aplicarán al resto de prefabs. Si no pulso aplicar, la modificación será únicamente para ese prefab.

Crearemos un prefab arrastrando el objeto deseado desde el panel de jerarquía hasta el panel de proyecto.

Para instanciar un prefab utilizaremos el siguiente código:

public GameObject prefabMisil;
...
GameObject nuevoMisil = Instantiate(prefabMisil, new Vector2(2,2), Quaternion.identity);

Coordenadas en jerarquías

Cuando tenemos una jerarquía (al hacer un prefab, por ejemplo) en la que hay un padre no visual (un empty, por ejemplo) y uno o varios hijos que si se ven (un cubo, por ejemplo), intentamos siempre ubicar al padre el centro de los hijos.

Alinear objetos Alinear objetos Alinear objetos

Instanciar un misil en una posición horizontal aleatoria

int pos = Random.Range(-4, 4);
GameObject nuevoMisil = Instantiate(prefabMisil, new Vector2(pos,4), Quaternion.identity);

Detectar colisiones

El jugador perderá cuando un misil desaparezca por la parte inferior de la pantalla.

Para detectar esto:

  1. Ponemos un cubo en la parte inferior de la pantalla y lo estiramos para que ocupe toda su longitud.
  2. Asignamos Box Collider 2D al cubo, en lugar del Box Collider que tenía. Para detectar la colisión entre dos entidades, estas deben tener el mismo tipo de Box Collider.
  3. En el componente Box Collider 2D del GameObject AreaInferior, activamos la check isTrigger.
  4. Vinculamos el siguiente Script al cubo inferior:
    public class AreaInferior : MonoBehaviour{
    	void OnTriggerEnter2D(Collider2D col){
    		Debug.Log("colision");
    	}
    }
Diferencia entre marcar o no marcar la check isTrigger de un BoxCollider.

Perder vidas

Cada vez que se produzca la colisión anterior, el usuario perderá una vida.

public class AreaInferior : MonoBehaviour
{
    private int marcador = 3;
    public Sprite corazonNegro;
    void OnCollisionEnter2D(Collision2D col)
    {
        marcador--;

        if(marcador == 2)
        {
            GameObject.Find("vida3").GetComponent<SpriteRenderer>().sprite = corazonNegro;
corazon negro

Perder el juego

Si el marcador de vidas llegase a cero, el jugador habrá perdido la partida y lanzaremos la escena correspondiente.

Te recuerdo el código para cambiar de escena que ya vimos en el juego de Elige tu propia aventura.

using UnityEngine.SceneManagement;
SceneManager.LoadScene ("Muerte");

Ejercicio: Galería de tiro

Primer paso - todo lo que ya vimos

  1. Colocamos el fondo y los enemigos con sus correspondientes animaciones en escena.
  2. Cuando pulsemos sobre cualquiera de los enemigos, debe mostrarse una traza.
  3. Finalmente, cambiaremos la traza por una animación de explosión.
enemigos con animaciones

Un único animal se mueve

Vector.Lerp hace una traslación desde un punto inicial hasta otro final. Cuando el t vale 0, el objeto estará en PosIni, cuando t vale 1, el objeto estará en PosFin y cuando t tiene cualquier valor intermedio entre 0 y 1, el objeto estará en algún punto intermedio proporcional

posicion = Vector2.Lerp(PosIni, PosFin, Tiempo/1)
public class GameController : MonoBehaviour {
	public GameObject[] personajes; 
	Vector2 posIni;
	Vector2 posFin;
	float t = 0;

	void Start(){
		posIni = new Vector2(personajes[0].transform.position.x,personajes[0].transform.position.y );
		posFin = new Vector2 (posIni.x + 35 , posIni.y);
	}

	void Update () {
		t += 0.1f * Time.deltaTime;
		personajes[0].transform.position = Vector2.Lerp (posIni, posFin, t);
		if (personajes[0].transform.position.x >= posFin.x) {
			personajes[0].transform.position = posIni;
			t = 0;
		}
	}
}

Detectando pulsación

Debemos hacer el objeto tenga un box collider.

Cuando un box collider va a tener movimiento, deberíamos añadirle un RigidBody. Esto evitará el recalculamiento del box collider cada vez que el objeto se desplace. La opción de Kinematic hará que las fuerzas no afecten al objeto.

void comprobarPulsacion(){
	Ray rayo = Camera.main.ScreenPointToRay (Input.mousePosition);
	RaycastHit hit;
	if (Physics.Raycast (rayo, out hit)) {
		if (hit.collider.gameObject == personajes[0].gameObject) {
			Debug.Log ("muerte");
		}
	}
}

Animación de muerte

hit.collider.gameObject.GetComponent ().SetBool ("vivo",false);

Varios animales se mueven

Hacer que sean varios animales los que se mueven ahora horizontalmente.

Si el número de animales es fijo, utilizaremos un array para añadirlos a pantalla. En este caso, si queremos evaluar cuando hemos pulsado sobre un animal, deberemos recorrelos todos con un bucle for.

Modificaremos la llamada a Vector2.Lerp para que las mascotas no comiencen superpuestas.

personajes[i].transform.position = Vector2.Lerp (posIni + Vector2.left * i*5, posFin + Vector2.left * i*5, t);

Contador de muertes

public class MascotaManagement : MonoBehaviour {
	...
	void OnMouseDown(){
		escena.incrementarMuertes ();
		anim.Play ("muerte");
	}
public class EscenaManagement : MonoBehaviour {
	...
	public void incrementarMuertes(){
		this.contadorMuertes = this.contadorMuertes + 1;
		GameObject.Find ("Texto").GetComponent<Text> ().text = ""+this.contadorMuertes;
	}

El GameObject "Texto" será un elemento UI que hay en escena. Tener en cuenta que si el texto que tiene dentro no cabe, no se verá. Para que si el texto no cabe, se vea, deberemos poner las propiedades Horizontal Overflow, y Vertical Overflow con valor Overflow.

Descargar ejercicio

Carga asíncrona de una escena

AsyncOperation asyncOperation;

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

IEnumerator cargarEscenaAsincronamente(){
	asyncOperation = SceneManager.LoadSceneAsync("Escena2", LoadSceneMode.Single);
	asyncOperation.allowSceneActivation = false;
	while (!(asyncOperation.progress > 0.89f)){
		Debug.Log("Progress: " + asyncOperation.progress);
		yield return null;
	}
	Debug.Log("llega el amor");
	yield break;
}

public void irAEscena2(){
	asyncOperation.allowSceneActivation = true;
}

Ejercicio

Carga ambas escenas asíncronamente

Corrutinas

Son códigos que se ejecutan a lo largo de un espacio de tiempo. Utilizan la clase IEnumerator. En este ejemplo, el script irá asociado a la cámara.

public class Corrutinas : MonoBehaviour {

	public GameObject[] cubos;

	void Start () {
		cubos = GameObject.FindGameObjectsWithTag ("Player");
		StartCoroutine (ApagarCubos());
	}

	IEnumerator ApagarCubos(){
		yield return new WaitForSeconds (2.0f);
		Debug.Log ("Empieza corrutina");
		cubos [0].SetActive (false);


		yield return new WaitForSeconds (2.0f);
		Debug.Log ("Empieza corrutina");
		cubos [1].SetActive (false);

		yield return new WaitForSeconds (2.0f);
		Debug.Log ("Empieza corrutina");
		cubos [2].SetActive (false);
	}
}
Descargar ejemplo

Flappy Bird

flappy bird

Ponemos la imagen de fondo

imagen fondo flappy bird con unity

Ponemos el heroe en pantalla

Debe caer por gravedad y tener una animación.

Cuando pulsamos la tecla espacio, el pájaro sube para arriba

Rigidbody2D heroeRB;
private int potenciaSalto = 3;
void Start(){
	heroeRB = GameObject.Find("heroe").GetComponent<Rigidbody2D>();
}

void Update(){
	if (Input.GetKeyDown(KeyCode.Space)){
		heroeRB.velocity = Vector2.zero; //Si pulsamos el botón de saltar justo cuando estamos cayendo, podría ser que se restasen las velocidad 
		heroeRB.AddForce(Vector2.up * potenciaSalto, ForceMode2D.Impulse);
	}
}

Una tubería se mueve horizontalmente

flappy bird con unity

Creamos un prefab (varios objetos que arrastramos simultaneamente al panel de proyecto) con dos tuberías enfrentadas cuyo origen estará en el punto 0.

public class Tuberia : MonoBehaviour{
    void Update(){
        this.transform.Translate(-2 * Time.deltaTime, 0, 0);
    }
}

Crear varias tuberías

Hacemos una corrutina para ir insertando las tuberías. Las corrutinas se lanzan con StartCoroutine.

public class GameController : MonoBehaviour{
    public GameObject tuberiaPrefab;
    void Start(){
        StartCoroutine(generarTuberias());
    }

    IEnumerator generarTuberias(){
        while (true){
            GameObject tuberiaNueva = Instantiate(tuberiaPrefab, new Vector2(10, 0), Quaternion.identity);
            tuberiaNueva.name = "tuberia";
            yield return new WaitForSeconds(3.0f);
        }
    }
}

Tuberías en posiciones aleatorias

int posicionY = Random.Range(-3,3);
GameObject tuberiaNueva = Instantiate(tuberiaPrefab, new Vector2(10, posicionY), Quaternion.identity);

Perder al colisionar contra una tubería

  1. Crearemos un objeto empty dentro de la tubería.
  2. Asociamos un BoxCollider2D al empty.
  3. Entre las propiedades del componente BoxCollider2D, le damos a Edit Collider, para ajustar el area de colisión del empty al del grupo de tuberías superior.
  4. Repetimos para el grupo de tuberías inferior.
  5. Asociamos al heroe un BoxCollider2D con Body Type Dynamic y la check isTrigger marcada.
  6. Asociamos el código necesario al heroe para detectar la colisión y cambiar de escena.
colisiones con tuberías en unity

Perder al salir por los límites de la pantalla

Simplemente añadimos dos cubos con su correspondiente BoxCollider2D a la parte superior e inferior de la pantalla.

Destruir tuberías

prefabTuberíaif (transform.position.x < -10) {
	Destroy (this.gameObject);
}

PONG

Ponemos el fondo en pantalla

fondo PONG

Ponemos la pelota en pantalla

pelota pong con Unity

Añadimos el script de movimiento a la pelota

public class Pelota : MonoBehaviour
{
    int speedX;
    int speedY;
    float speed;

    void Start()
    {
        speed = Random.Range(5, 10);
        //La siguiente línea nos da un valor aleatorio que puede ser 0 o 1;
        speedX = Random.Range(0, 2);
        if (speedX == 0) speedX = -1;

        speedY = Random.Range(0, 2);
        if (speedY == 0) speedY = -1;

        //Para pasar la velocidad usamos un vector3 porque le pasamos 3 parámetros: x, y, z
        GetComponent<Rigidbody2D>().velocity = new Vector2(speedX * speed, speedY * speed);
    }
}

Hacemos que la pelota rebote contra los límites superior e inferior de la pantalla

  1. Colocamos dos cubos estirados en la parte superior e inferior de la pantalla.
  2. Asignamos un BoxCollider2D a los cubos.
  3. Asginamos un BoxCollider2D a la pelota, con Body Type: Dynamic y Gravity Scale:0.
  4. En el panel de proyecto, creamos un nuevo Physics Material 2D. A este material le asignamos estos valores:
    • Friction: 0
    • Bounciness: 1

Poniendo en pantalla las palas.

Las palas deberían de tener un area colisionable, de tal forma que la pelota rebote contra ellas.

Esto ya lo deberías saber hacer tú :D.

Moviendo las palas

Detectar la pulsación de las teclas:void Update() {
	if (Input.GetKey(KeyCode.A)){
		...
	}if (Input.GetKey(KeyCode.D)){	
		...
	}
}

En lugar de declarar un GameObject y acceder a su propiedad transform (lo cual es equivalente a hacer un GetComponent<Transform>() y por tanto, consume recursos) declararemos un objeto de tipo Transform en su lugar.

Desplazar al player:public Transform pala1;
...
pala1.Translate(0, 2 * Time.deltaTime, 0);

Limitando el movimiento de las palas

float limiteSuperior, limiteInferior;

void Start(){
    limiteInferior = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, 0))[1]+pala1.GetComponent<BoxCollider2D>().size.y/2;
    limiteSuperior = Camera.main.ViewportToWorldPoint(new Vector3(0, 1, 0))[1]-pala1.GetComponent<BoxCollider2D>().size.y / 2;
}

if (Input.GetKey(KeyCode.Q) && pala1.transform.position.y < limiteSuperior)

Mejoras adicionales

Carreras

Ponemos la imágen de fondo

Fondo juego carreras con Unity

Ponemos un player en pantalla

Lo pondremos con su correspondiente animación.

Juego carreras con Unity 3D

El jugador avanza hacia delante

No queremos el el player caiga por gravedad, así que, para su componente RigidBody2D:

void Update(){
	rb.velocity = transform.right * speed;
}

Girar al jugador con el teclado

Time.deltaTime ejecuta el cambio especificado en el tiempo concreto de un segundo.

transform.Rotate gira el objeto los grados especificados, mientras que transform.rotation establece el valor del transform sin tener en cuenta su rotación anterior.

void Update() {
	if (Input.GetKey(KeyCode.A)){
		transform.Rotate(0,0,90 * Time.deltaTime);
	}if (Input.GetKey(KeyCode.D)){	
		transform.Rotate(0, 0, -90 * Time.deltaTime);
	}
	rb.velocity = transform.right * speed;
}

No queremos que en el siguiente paso el decorado dote de un impulso giratorio al prota, por lo que tenemos dos opciones:

Colisión con decorado

Puesto que el decorado de este juego tiene formas muy irregulares, le asignaremos un Polygon Collider 2D, que automáticamente detectará las zonas transparentes del sprite para definir el area colisionable.

Añadimos cuatro jugadores

Consideraciones:

Colocar marcador

Para cada jugador añadimos dos objetos de tipo TeshMeshPro. Uno que identificará al jugador y otro para su marcador. Tener en cuenta que serán objetos de tipo 3D.

Para evitar que el marcador se posicione detrás de alguno de los objetos de decorado ya situados en escena, en este caso no podremos cambiar su Order in Layer puesto que es un objeto de tipo 3D. Lo que debemos hacer es asegurarnos que el Order in Layer del resto de los objetos sea negativo, ya que el Order in Layer de un objeto 3D es cero.

Detectar paso por la línea de meta

Debemos detectar la colisión entre cualquiera de los jugadores y un empty con un BoxCollider2D. Para evitar la detección de la colisión entre los jugadores y cualquier objeto del decorado, etiquetaremos a la meta como Finish y en el código que gestiona la colisión evaluaremos esa etiqueta:

etiquetado unity 3d
void OnTriggerEnter2D(Collider2D col){
	if(col.gameObject.tag == "Finish"){
		Debug.Log("ha pasado un player");
	}
}

Evitar trampas

Podría darse el caso de que un jugador atravesase varias veces seguidas la línea de meta sin completar la vuelta. Para evitar esto, colocaremos en pantalla varios BoxCollider2D el el check isTrigger activado. La idea es que el jugador debe pasar por cada uno de estos BoxCollider2D antes de pasar por la línea de meta.

markers en juego carreras de Unity 3d
void OnTriggerEnter2D(Collider2D col){
	if (col.gameObject.name == "marker1" && marker == 0){
		marker++;
	}else if (col.gameObject.name == "marker2" && marker == 1){
		marker++;
	}else if (col.gameObject.name == "marker3" && marker == 2){
		marker++;
	}else if (col.gameObject.name == "marker4" && marker == 3){
		marker++;
	}else if (col.gameObject.tag == "Finish" && marker == 4){
		marker = 0;
		Debug.Log("Un player ha dado una vuelta");
	}
}

Aumentar el contador de vueltas

public TextMeshPro marcadorAsociado;
...
marcadorAsociado.text = ""+marcadorVueltas;

Terminar la partida al llegar a las dos vueltas

Condiciones del panel animator

Es posible crear parámetros (panel animator -> pestaña parameters) y vincularlos a las transiciones que unen:

  1. 'Any state' y una animación
  2. Dos animaciones

Lo que no hacemos es vincularlo a una transición entre 'Entry' y cualquier otro estado o animación.

Para que una animación deje de ejecutarse, debemos definir una transición de salida a otra animación. En este caso, la animación de salida será 'idle' (el cubo en su posición inicial);

De esta forma, las animaciones se ejecutrarán sólo si se cumplen ciertas condiciones (que podemos definir en el inspector al seleccionar una transición).

unity3d panel animator

Tener en cuenta:

  1. si en el listado de condiciones que aparece en el panel inspector al seleccionar una transición, tenemos una condición asociada a un hueco vacío (en el lugar en el que debería aparecer el parámetro), las otras condiciones no funcionarán.
  2. si la check 'Can transition to self' de settings que se muestra en el inspector cuando tenemos una transición seleccionada está desmarcada, evitaremos que la animación se ejecute de nuevo hasta que no haya salido de ese estado.

Ejercicio: Animación sin Scripting

Crear tres animaciones vinculadas a un cubo.

ejercicio panel animator en unity 3d

Luego, en el panel animator, añadir parámetros y finalmente establecer condiciones en las transiciones para que estas se ejecuten sólo para valores concretos de los parámetros.

Plataformas

Ponemos la imagen de fondo

imagen de fondo juego con Unity 3D

Ponemos al prota en pantalla

player juego plataformas

Tilemap Editor

Crear un nuevo tilemap

  1. Botón derecho sobre el panel de jerarquía -> 2D Object -> Tilemap
  2. Menú Window -> 2D -> Tile Palette
  3. New Palette -> Seleccionamos los tiles independientes -> Se creará una nueva paleta con los tiles seleccionados.

Insertar tiles en la escena

  1. Habrá que ajustar el grid size del grid para que se corresponda con el de los tiles que estamos añadiendo en pantalla. Por ejemplo, si el tamaño de los tiles que estamos insertando es de 32x32px, el grid size será X:0.32 e Y:0.32. unity3d cell size
  2. El punto origen del tile debería ser su centro, así que nos aseguramos de que tenemos los valores para el anchor con 0.5. unity 3d tile anchor
  3. Deberemos tener seleccionada la herramienta de pintar. pintar tiles en unity
  4. Si quiero desplazar tiles dentro del tile palette, debo:
    1. Tener la opción de edit seleccionada
    2. Seleccionar el tile con la herramienta de selección.
    3. Desplazar el tile con la herramienta de desplazar
    desplazar tiles en unity 3d

    Insertar tiles colisionables

    1. Seleccionamos el grid y le añadimos un nuevo tilemap que contendrá los tiles que sean colisionables.
    2. A este nuevo tilemap le añadimos el componente Tilemap Collider 2D.
    3. Los tiles que añadamos a la escena y que provengan de este tilemap, tendrán colisión.
    4. Recuerda que para que la colisión funcione, el player tenga un Box Collider 2D.

    Desplazamiento horizontal

    Para gestionar el movimiento del personaje podemos:

    • Usar fuerzas (como en este ejemplo, usando el RigidBody).
    • No usar fuerzas (utilizando transform.position para gestionar el desplazamiento). Para este caso es una solución peor, ya que cuando el jugador colisione contra una pared, lo que va a ocurrir es que se va a introducir dentro ligeramente. Es como si fuese un balón de playa que se introduce dentro del agua y luego es expulsado. Esto no ocurre cuando usamos fuerzas.
    o no

    • El protagonista tendrá el componente Capsule Collider 2D. Usaremos un Capsule Collider en lugar de un Box Collider porque en este juego en el que estamos utilizando tiles, a veces el box colider colisiona contra una esquina saliente de un tile, impidiendo el desplazamiento del personaje. Esto es un error de unity, ya que realmente los tiles estarían todos a la misma altura.
    • El protagonista tendrá, además, el componente Rigidbody2D.
    • Es posible controlar las teclas asociadas al Input Horizontal en Edit -> Project Settings -> Input -> Axes -> Horizontal
    • Si intentamos usar las flechas de dirección para mover al personaje, es probable que tengamos problemas. Tenemos dos opciones:
      • Usar las teclas a y d.
      • Jugar con la pantalla de juego maximizada (shift + space).
    private int walkspeed = 3;
    private float horInput;
    private Vector2 movement;
    private Rigidbody2D rb;
    
    //Esto se ejecuta más veces por segundo. Lo usaremos para evaluar las físicas
    void FixedUpdate(){
    	this.horInput = Input.GetAxis ("Horizontal");
    	//Recogemos la velocidad actual del rigid body, para que no se resetee el valor de y cuando asignemos la velocity
    	this.movement = this.rb.velocity;
    	this.movement.x = horInput * walkspeed;
    	this.rb.velocity = this.movement;
    }

    Diferencia entre el FixedUpdate y Update

    El FixedUpdate se utiliza para que en ordenadores con distinta potencia, las fuerzas se apliquen el mismo número de veces por segundo. Por tanto, lo usaremos cuando haya una fuerza constante que se está ejecutando. En este caso, la fuerza que hace que el protagonista se desplace.

    El método Update lo usaremos para escuchar, por ejemplo, eventos que se producen de forma puntual e inmediata, como un salto o disparar una bala.

    Salto (se puede saltar muchas veces)

    private bool jumpInput;
    
    void Update(){
    	jumpInput = Input.GetButtonDown("Jump");
    }
    
    void FixedUpdate(){
    	if (jumpInput){
    		movement.y = jumpImpulse;
    	}

    En Rigid Body -> Constraints pondremos restricciones para la rotación para que el jugador no pueda caer de cabeza.

    Se puede saltar sólo cuando estamos en contacto con el suelo

    Debemos asignar un tag al tilemap.

    private bool grounded;
    
    	...
    	if (this.jumpInput && grounded) {
    	...
    	
    void OnCollisionEnter2D(Collision2D col ){
    	if (col.gameObject.tag == "suelo") {
    		grounded = true;
    	}
    }
    
    //Ponemos el grounded a false en este método en lugar de cuando saltamos, para que el salto deje de funcionar si caemos por un precipicio (en este caso perdemos el contacto con el suelo aunque no hemos saltado)
    void OnCollisionExit2D(Collision2D col){
    	if (col.gameObject.tag == "suelo") {
    		grounded = false;
    	}
    }

    Añadir animación de andar

    En este caso, gestionaremos la activación de las distintas animaciones utilizando parámetros desde el panel animator.

    unity 3d animator plataformas

    Para que las transiciones entre una animación y otra se produzcan de manera instantánea cuando el usuario pulse las teclas de movimiento, deben seleccionar dichas transiciones en el panel animator y cambiar algunos de sus parámetros en el panel inspector:

    transiciones instantáneas unity 3d

    En lugar de llamar a SetBool constantemente, lo haremos sólo cuando sea necesario, es decir cuando haya un cambio entre estar corriendo y dejar de correr. Hacemos esto porque el método SetBool es costoso a nivel de rendimiento.

    // Si el jugador no estaba corriendo y se pone a correr
    // y si el jugador estaba corriendo y deja de correr
    // cambio la animación de correr
    if((horInput != 0.0f && !corriendo) ||(horInput == 0 && corriendo)){
    	corriendo = !corriendo;
    	anim.SetBool("corriendo", corriendo);
    }
    if (jumpInput > 0 && grounded){
    	anim.SetBool("saltando", true);
    	movement.y = jumpImpulse;
    }

    Hacer que el prota encare la dirección correcta

    if (horInput < 0) this.facing = true;
    else if(horInput > 0) this.facing = false;
    this.spr.flipX = facing;

    Añadir un enemigo que está quieto

    Enemigo que camina

    enemigovoid Update () {
    	transform.Translate (direccion*velocidad * Vector2.right * Time.deltaTime, 0);
    }

    El enemigo cambia de dirección

    void Start(){
    	spr = GetComponent<SpriteRenderer>();
    	boxCol = GetComponent<BoxCollider2D>();
    	float margenRayo = 0.2f;
    	distanciaRayo = boxCol.size.x / 2 + margenRayo;
    }
    
    void Update(){
    	...
    	Vector2 puntoOrigenHaciaAbajo = new Vector2 (this.transform.position.x  + direccion * distanciaRayo, this.transform.position.y);
    	RaycastHit2D hitAbajo= Physics2D.Raycast(puntoOrigenHaciaAbajo, Vector2.down,2f);
    	Debug.DrawRay (puntoOrigenHaciaAbajo, Vector2.down, Color.red);
    
    	if(hitAbajo.collider == null){
    		spr.flipX = (direccion == 1) ? false : true; 
    		direccion = direccion * -1;
    	}
    }
    rayo enemigo

    Colisión contra el enemigo

    El siguiente código hace que cuando impactemos contra un enemigo salgamos despedidos en dirección contraria.

    Player.csvoid OnCollisionEnter2D(Collision2D col){
    	...
    	if (col.gameObject.tag == "enemigo") {
    		controlesActivos = false;
    		CancelInvoke ("Rehabilitar"); //Cancelamos todas las invocaciones del método rehabilitar que pudiesemos tener de antes.
    		Invoke ("Rehabilitar", 0.5f);
    		float direccionEmpuje = Mathf.Sign (this.gameObject.transform.position.x - col.gameObject.transform.position.x);
    		Debug.Log ("Direccion empuje = " + direccionEmpuje);
    		rb.velocity = new Vector2(direccionEmpuje*10f, 10f);;
    		Debug.Log (rb.velocity);
    	}
    }
    void Rehabilitar(){
    	controlesActivos = true;
    }
    
    //Sólo podremos movernos si no estamos volando tras colisionar con un enemigo
    if (controlesActivos == true ) {
    	movement.x = horInput * walkspeed;
    
    	if (jumpInput && grounded ) {
    		movement.y = jumpImpulse;
    	}
    	rb.velocity = this.movement;
    }

    Balas

    Bala.csvoid Start(){
    	...
    	// transform.right tiene en cuenta la rotación local del objeto
    	rb.AddForce(transform.right * velocidadInicial, ForceMode2D.Impulse);
    }
    Player.csvoid Update(){
    	...
    	fire1 = Input.GetButtonDown("Fire1"); // Por defecto Fire1 es left ctrl
    	...
    }
    
    void Disparar(){
    	Instantiate (balaPrefab, transform.position, balaPrefab.transform.rotation) as GameObject;
    }

    Marcaremos la check is Trigger del box collider para que las balas no empujen al player que las está disparando.

    La bala se mueve en la dirección correcta

    Utilizaremos un código similar a este para el protagonista:

    Player.csvoid Disparar(){
    	Quaternion rotacion = facing ? Quaternion.Euler(new Vector3(0, 180, 0)) : Quaternion.Euler(new Vector3(0, 0, 0));
    	Instantiate(balaPrefab, transform.position, rotacion);
    }

    Plataforma móvil I

    Utilizaremos un empty con dos hijos: la plataforma que voy a mover y el punto de destino. A la plataforma que queremos mover le asociaremos el siguiente script. La propiead target será el destino del movimiento, que irá cambiando según lo alcancemos.

    plataforma móvil con unity 3d
    PlataformaDinamicapublic class PlataformaDinamica : MonoBehaviour {
    	public Transform target;
    	public float speed;
    	private Vector3 start,end;
    
    	void Start () {
    		if (target != null){
    			start=transform.position;
    			end=target.position;
    		}	
    	}
    
    	void FixedUpdate(){
    		if (target != null){
    			float fixedSpeed = speed * Time.deltaTime;
    			transform.position=Vector3.MoveTowards(transform.position, target.position, fixedSpeed);
    		}
    //comprobamos si la posicion final es igual al ppio, target position vale end y si no, vale start
    		if (transform.position == target.position){
    			target.position= (target.position ==start) ? end: start; //cambiamos la posicion del target
    		}	
    	}
    }

    Si queremos ver dibujada en pantalla la línea de la trayectoria, podemos vincular al objeto, además, la siguiente clase

    DrawSceneLine.cspublic class DrawSceneLine : MonoBehaviour {
    
    	public Transform from; //origen del desplazamiento
    	public Transform to;//final del desplazamiento
    
    	void OnDrawGizmosSelected(){
    
    		if (from != null && to != null){
    			Gizmos.color=Color.cyan;
    			Gizmos.DrawLine(from.position,to.position);
    			Gizmos.DrawSphere(from.position, 0.15f);
    			Gizmos.DrawSphere(to.position, 0.15f);
    		}
    	}
    }

    Plataforma móvil II - sin colisión cuando entramos desde abajo

    1. En el Box Collider de la plataforma, marcamos la check Used By Efector
    2. Añadimos a la plataforma el componente Platform Effector 2D. Definimos un surface arc de menos de 180º.

    Plataforma móvil III - Evitar que la plataforma "escupa" al prota

    Cuando el player entre en la plataforma desde abajo, no habrá colisión. Cuando entre desde arriba, haremos al player hijo de la plataforma.

    PlataformaMovil.csvoid OnCollisionEnter2D(Collision2D col){
    	if (col.transform.tag == "Player"){
    		if (col.transform.position.y > transform.position.y){
    			col.transform.parent = transform;
    		}
    	}
    }
    
    void OnCollisionExit2D(Collision2D col) {
    	if(col.gameObject.tag == "Player"){
    		col.transform.parent = null;
    	}
    }

    Evitar que el Player se quede enganchado al ir contra una plataforma

    Evitar que el player se quede enganchado al ir en la dirección de una plataforma
    Player.csLayerMask capasAfectadasPorElRayo;
    
    void FixedUpdate(){
    	...
    	if ((this.horInput < 0 && !PuedeMoverseIzquierda())
    	|| (this.horInput > 0 && !PuedeMoverseDerecha())){
    		this.movement.x = 0;
    	}
    	...
    }
    
    
    bool PuedeMoverseIzquierda(){
    	int cantidadRayos = 10;
    	//Usamos el skinWidth para que el rayo que sale de los pies y el rayo
    	//que sale de la cabeza no choquen contra el suelo ni el techo
    	float skinWidth = 0.1f;
    	float distanciaEntreRayosDetectores = (cc.size.y - skinWidth*2) / cantidadRayos;
    	float coordenadaYDelRayo = transform.position.y - (cc.size.y / 2) + skinWidth;
    
    	for (int i = 0; i <= cantidadRayos; i++){
    		Vector3 puntoOrigenRayo = new Vector3(transform.position.x - cc.size.x / 2, coordenadaYDelRayo, 0);
    		RaycastHit2D hit = Physics2D.Raycast(puntoOrigenRayo, Vector2.left, longitudRayo, capasAfectadasPorElRayo);
    		Debug.DrawRay(puntoOrigenRayo, Vector2.left, Color.red);
    		if (hit){
    			return false;
    		}
    		coordenadaYDelRayo += distanciaEntreRayosDetectores;
    	}
    	return true;
    }
    
    // Faltaría programar el método PuedeMoverseDerecha();

    No queremos que los rayos colisionen con el propio personaje, así que definimos una layermask en la que no incluímos al Player.

    layer mask con unity 3d

    Barra de vida

    ControlPlayerpublic BarraVita barraVita;
    barraVita.actualizarVidas (vidas, maximoVidas);

    BarraVita será el script asociado a un objeto image del canvas al que habremos asociado como sprite un punto blanco. Seleccionaremos en su componente Image:

    • Image Type: Filled
    • Fill Method: Horizontal
    BarraVita.cspublic class BarraVita : MonoBehaviour {
    	private Image barraVida;
    
    	void Start () {
    		barraVida = this.GetComponent<Image> ();
    	}
    
    	public void actualizarVidas(int vidas, int maximoVida){
    		barraVida.fillAmount = vidas*1.0f / maximoVida;
    	}
    }

    Coprota sigue a prota

    Coprota sigue a prota
    public class CoProta : MonoBehaviour {
    
    	private Transform destino;
    	private Player playerCodigo;
    	void Start () {
    		GameObject player = GameObject.Find("Player");
    		playerCodigo = player.GetComponent<Player>();
    		destino = player.transform.Find("DestinoCoprota");
    	}
    
    	void Update(){
    		//Cambiar destino del coprota dependiendo de tu direccion
    		destino.localPosition = new Vector3(Mathf.Abs(destino.localPosition.x) * playerCodigo.facing * -1, destino.localPosition.y, destino.localPosition.z);
    	}
    
    	void FixedUpdate(){
    		//El tercer parámetro del Lerp indica el porcentaje de la ruta que hace en cadda frame
    		transform.position = Vector2.Lerp(transform.position, destino.position, 0.1f);
    	}
    }
    Descargar recurso

    Puzzle con RayCast

    Seleccionar una pieza (lanzar un rayo 2D y 3D)

    Para que funcionen los rayos, cada pieza debe tener un BoxCollider y un RigidBody.

    GameObject pieza;
    
    void Update(){
    	if(Input.GetMouseButtonDown(0)){
    		seleccionarPieza ();
    	}
    }
    
    void seleccionarPieza(){
    	//Si estamos trabajando con Colliders 2D
    	//RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
    	//if (hit)
    
    	//Si estamos trabajando con Colliders 3D
    	Ray rayo = Camera.main.ScreenPointToRay (Input.mousePosition);
    	RaycastHit hit;
    	if (Physics.Raycast (rayo, out hit)) {
    		if (hit.collider.gameObject.tag == "piezapuzzle") {
    			distanciaRayo = hit.distance; //distanciaRayo será un valor que utilizaremos más adelante
    			pieza = hit.collider.gameObject;
    			Debug.Log (pieza);
    		}	
    	}
    	
    }

    Mover pieza con RayCast

    void seleccionarPieza(){
    	...
    	estaPulsado=true;
    	...
    }	
    
    void Update(){
    	if(estaPulsado){
    		moverPieza ();
    	}	
    }
    
    void moverPieza(){
    	Ray rayo = Camera.main.ScreenPointToRay (Input.mousePosition);
    	Vector3 limiteRayo = rayo.GetPoint (distanciaRayo);
    	limiteRayo = new Vector3 (limiteRayo.x, limiteRayo.y, 0);
    	pieza.transform.position = limiteRayo;
    }

    Explicación del método mover pieza.

    Un rayo es infinito. Nos interesa mover la pieza al punto correcto de profundidad; por eso utilizamos la variable longitudRayo.

    limiteRayo será el segmento que va desde la cámara, hasta el punto en que colisionamos con el bounding box de la pieza. Sin embargo, si la pieza tiene profundidad z=0, la bounding box tendrá una profundidad diferente y cada vez que llamemos a moverPieza() cambiaremos su ubicación, porque el origen de coordenadas de la pieza no coincide con el punto de impacteato, y la pieza se desplazará.

    Unity 3D bounding box

    Poner las piezas que se están moviendo por delante

        void seleccionarPieza(){
    	...
    	if (hit){
    		ultimaFichaOrden++;
    		pieza.transform.GetChild(0).GetComponent<SpriteRenderer>().sortingOrder = ultimaFichaOrden;
    		pieza.transform.GetChild(1).GetComponent<TextMeshPro>().sortingOrder = ultimaFichaOrden;

    Arrastrar y soltar

    ...
    	if (Input.GetMouseButtonUp (0)) {
    		soltarPieza ();
    	}
    ...
    void soltarPieza(){
    	if (estaPulsado) {
    		estaPulsado = false;
    		pieza = null;
    	}
    }

    Comprobar si he dejado la pieza sobre un area de la pantalla

    Evaluar la distancia entre una ficha y otra en el momento de soltarla

    public List<GameObject> fichasMoviles;
    public List<GameObject> fichasSolucion;
    public void ComprobarDrop(){
    	bool acierto = false; ;
    	for (int i = 0; i < fichasSolucion.Count; i++){
    		if (Vector2.Distance(pieza.transform.position, fichasSolucion[i].transform.position) < 1){
    			if (pieza.name == fichasSolucion[i].name){
    				fichasSolucion[i].transform.GetChild(1).GetComponent<TextMeshPro>().text = pieza.name;
    				Destroy(pieza.gameObject); //ficha seleccionada y ficha solucion son gameObjects
    				fichasSolucion.RemoveAt(i);
    				contadorSolucionesCorrectas++;
    				if (contadorSolucionesCorrectas == 10){
    					Debug.Log("Ganaste");
    				}
    				acierto = true;
    				break;
    			}   
    		}   
    	}
    	if (!acierto){
    		colocarPiezaEnPosicionesIniciales();
    	}
    }

    Comparar la posición poniendo el código en la pieza en lugar de en el gamecontroller

    Para que funcionen los métodos OnTriggerEnter y OnTriggerExit debe estar marcada la check Is Trigger del componente Box Collider.

    También debemos asignar un Rigid Body a los objetos que van a moverse.

    Piezapublic class Pieza : MonoBehaviour {
    	
    	public bool estaTocando;
    
    	void OnTriggerEnter(Collider col){
    		if (name=="piezaRosa" && col.GetComponent<Collider>().gameObject.name == "rosita") {
    			estaTocando = true;
    		}	
    
    		if ( name=="piezaMorada" && col.GetComponent<Collider>().gameObject.name == "morado") {
    			estaTocando = true;
    		}
    	}
    
    	void OnTriggerExit(Collider col){
    		if (name=="piezaRosa" && col.gameObject.name == "rosita" ) {
    			estaTocando = false;
    		}
    		if (name=="piezaMorada" && col.gameObject.name == "morado") {
    			estaTocando = false;
    		}
    	}
    }
    ControlEscenavoid comprobarCubos(){
    	if (pieza.GetComponent<Pieza> ().estaTocando == true) {
    		Debug.Log ("toca");
    		Destroy (pieza);
    	}
    }

    Destroy con transición

    pieza.GetComponent<Animator> ().enabled = true;
    Destroy (pieza, 1.0f);

    Encajar piezas

    public float margenError = 2;
    public Vector3 [] posicionesFinales;
    
    void Start(){
    	...
    	posicionesFinales = new Vector3[piezas.Length];
    	for (int i = 0; i < piezas.Length ; i++) {
    		posicionesFinales [i] = piezas [i].transform.position;
    	}
    }
    void soltarPieza(){
    	if (estaPulsado) {
    		estaPulsado = false;
    		colocar ();
    		pieza = null;
    	}
    }
    
    void colocar(){
    	int numAux = 0;
    	for (int i = 0; i < piezas.Length; i++) {
    		if (piezas [i].gameObject == pieza) {
    			Debug.Log ("coinciden");
    			numAux = i;
    			break;
    		}
    	}
    	if (Vector3.Distance (piezas [numAux].position, posicionesFinales [numAux]) <= margenError) {
    		Debug.Log ("encaja "+ numAux);			
    		piezas[numAux].position= posicionesFinales [numAux];
    	}	
    }

    Desordenar piezas

    void desordenar(){
    	for (int i = 0; i < piezas.Length; i++) {
    		piezas [i].transform.position = new Vector3 (Random.Range(-5,5), Random.Range(-2,2), 0);
    	}
    }

    Desordenar piezas al cabo de un tiempo

    Invoke("desordenar", 2.0f);

    Juego arrastrar y soltar

    Para hacer la comparación de si el valor de la mariquita coincide con el valor de la hoja, lo más fácil será modificar dinámicamente el name de cada una de las mariquitas y de cada una de las hojas, asignándoles el valor solución. Luego, compararemos los name de ambos objetos.

    El objeto TeshMeshPro que almacena la solución de las hojas, es hijo de un dibujo de circunferencia que a su vez es hijo de una hoja. Para acceder al objeto padre desde el objeto hijo, usaremos el siguiente código:

    void Start () {
    	float altoPantalla = Camera.main.orthographicSize * 2;
    	float anchoPantalla = altoPantalla * Screen.width / Screen.height ;
    	float separacionEntrePiezasBTN = anchoPantalla / (botonesSUP.Length +1);
    	float escalaBTN = separacionEntrePiezasBTN * 0.1f;
    
    	distribuirHorizontalmente (botonesSUP,anchoPantalla, separacionEntrePiezasBTN,escalaBTN, 0);
    }
    
    void distribuirHorizontalmente(SpriteRenderer[] botones, float anchoPantalla, float separacionEntrePiezas, float escala,int nFila){
    	for (int i=0; i < botones.Length; i++) {
    		botones[i].transform.position = new Vector3(separacionEntrePiezas + separacionEntrePiezas*i- anchoPantalla/2, separacionEntrePiezas*nFila ,0);
    		botones [i].gameObject.transform.localScale = Vector3.one *escala;
    	}
    }

    Dibujar líneas con el mouse

    1. Configuración global:
      1. Usaremos una vista ortográfica para la cámara. ( Seleccionamos la cámara -> Projection -> Orthographic)
      2. Usaremos un fondo plano (Seleccionamos la cámara -> Clear flags -> Solid color)
    2. Creando el pincel:
      1. Create Empty. Lo llamaremos ball
      2. Dentro de este empty creamos un 2D Object -> Sprite. Le asignamos la imagen del pincel:
        Pincel unity 3d
      3. Asociamos un Rigid Body 2D al objeto Ball.
      4. Dentro del componente Rigid Body 2D cambiamos la propiedad Body Type a static para que la pelota no caiga por gravedad.
      5. Añadimos un Circle Collider 2D al objeto Ball y lo ajustamos al tamaño del pincel.
      6. Convertimos el objeto Ball en un Prefab.
    3. Creamos la línea
      1. Creamos un nuevo empty que llamaremos Line.
      2. Añadimos un componente de tipo Line Renderer a esta línea.
      3. Creamos un nuevo material y lo vinculamos al objeto.Line.
      4. Dentro del componente Line Renderer del objeto Line:
        1. Asignaremos un Width de 0.15.
        2. Asignamos un Edge Collider 2D a la línea.
        3. Salvamos la línea como Prefab.
    4. Creamos un nuevo Script llamado DrawLine
    5. EscenaManagement.csvoid Start () {
      	//Creamos los números aleatorios que iran en las piezas de arriba
      	for (int i = 0; i < 10; i++) {
      		nAleatorios [i] = UnityEngine.Random.Range (0, 51);
      	}
      	//Creamos y asignamos los números aleatorios a las piezas de abajo
      	nAleatoriosOrdenados = nAleatorios;
      	Array.Sort (nAleatoriosOrdenados);
      
      	for (int i = 0; i < botonesSOL.Length; i++) {
      		botonesSOL[i].GetComponent<Sol> ().valor = nAleatoriosOrdenados [i];		
      	}
      ...
    6. Creamos un Empty llamado GameController y le vinculamos el código anterior.
    7. Luego, en el panel inspector, iremos asociando las propiedades que hemos hecho públicas. Sólo con rellenar la primera propiedad ( Line Prefab ya podremos dibujar).
    Descargar programa

    Plataformas con RayCast

    Creamos un nuevo proyecto 2D.

    Cuando terminemos...

    • La entidad player tendrá asociadas las clases Player, PlayerInput, Controller2D (cuya propiedad Collision Mask será obstacle) y pertenecerá al layer player
    • La entidad MainCamera tendrá asociada la clase CameraFollow.
    • Las plataformas y el suelo tendrán asociadas el Layer obstacle.
    • Debemos asegurarnos de que la check Edit -> Project Settings -> Physics2D -> Auto Sync Transforms está marcada.
    Descargar código completo

    Configurando el proyecto

    En Edit -> Project Settings -> Physics 2D ...

    • Desmarcamos la check Reuse Collision Callbacks
    • Marcamos la check Auto Sync Transforms
    Configuración rayos en Unity

    El cubo cae por gravedad

    Vincularemos un cubo a la clase Player. Esta clase, implementará a su vez el componente Controller2D, que es el que se ocupa de la mayoría de la lógica.

    Pieza.csvoid OnTriggerEnter(Collider col){
    	//Si colisiono con un objeto etiquetado como "sol"...
    	if ( col.GetComponent().gameObject.tag == "sol") {
    		estaTocando = true;
    		//... obtengo el valor del propio objeto ...
    		string txtBtnArriba = gameObject.transform.GetChild(0).GetComponent<TeshMesh> ().text;
    		num = IntParseFast (txtBtnArriba);
    		//... y del objeto con el que colisiono ...
    		int  valueBtnAbajo = col.transform.GetComponent<Sol> ().valor;
    		col.transform.GetComponent<SpriteRenderer>().color=Color.red;
    		esCorrecto = false;
    		//..los comparo
    		if (num == valueBtnAbajo) {
    			esCorrecto = true;
    		}
    	}
    }
    ...
    public GameObject goSOL;
    ...
    goSOL = col.gameObject;
    ...
    goSOL.transform.GetChild (0).GetComponent<TextMesh> ().text = "" + pieza.GetComponent<Pieza> ().num;

    El cubo con los rayos

    A partir de la ubicación del cubo, el método VerticalCollisions se encarga de lanzar unos rayos (raycast) verticales hacia abajo. Para ello necesitaremos el punto de origen (raycastOrigin) y la dirección de cada uno de esos rayos.

    Estos rayos nos devolverán más adelante cuál es el objeto con el que están colisionando.

    No olvides que es necesario estar usando un BoxCollider2D

    public class Ficha : MonoBehaviour{
    	private bool enDrag = false;
    	private SpriteRenderer spriteRenderer;
    	private Juego4Numeros gc;
    	private Renderer texto;
    
    	void Start(){
    		gc = GameObject.Find("GameController").GetComponent<Juego4Numeros>();
    		//texto es nuestro texto 3D. Aunque no podemos establecer su sortingOrder desde el inspector de propiedades, podremos hacerlo mediante código
    		texto = this.transform.GetChild(0).GetComponent<Renderer>();
    		spriteRenderer = GetComponent<SpriteRenderer>();
    	}
    
    	void OnMouseDown(){
    		enDrag = true;
    		gc.ordenGlobalDeLasCapas++;
    		//Si queremos que cada vez soltemos la pieza, esta se coloque sobre el resto, necesitaremos una variable global para almacenar el orden de la capa que está más por encima
    		texto.sortingOrder = gc.ordenGlobalDeLasCapas;
    		spriteRenderer.sortingOrder = gc.ordenGlobalDeLasCapas;
    	}
    
    	void Update(){
    		if (enDrag){
    			Vector3 posRaton = Input.mousePosition;
    			transform.position = (Vector2)Camera.main.ScreenToWorldPoint(posRaton);
    		}
    		if (Input.GetMouseButtonUp(0) && enDrag){
    			enDrag = false;
    		}
    	}
    }
    objetoHijo.transform.parent.parent

    Skin Width

    Con el objetivo de que cuando el player esté descansando sobre el suelo y por tanto no haya separación con él podamos seguir lanzando un rayo vericalmente hacia abajo, vamos a aplicar una pequeña seaparación de los rayos respecto a los límites del cubo para que estos no salgan desde sus límites, sino desde un poco más adentro.

    cubo con rayos en unity 3d
    public class DrawLine : MonoBehaviour {
    	public GameObject linePrefab;
    	private GameObject currentLine;
    	private LineRenderer lineRenderer;
    	private EdgeCollider2D edgeCollider;
    	private List<Vector2> fingerPositions = new  List<Vector2>();
    
    	void Update () {
    		if (Input.GetMouseButtonDown(0)){
    			CreateLine();
    		}
    		if (Input.GetMouseButton(0)){
    			Vector2 tempFingerPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    			if (Vector2.Distance(tempFingerPos, fingerPositions[fingerPositions.Count - 1]) > .1f){
    				UpdateLine(tempFingerPos);
    			}
    		}
    	}
    
    	void CreateLine(){
    		currentLine = Instantiate(linePrefab, Vector3.zero, Quaternion.identity);
    		lineRenderer = currentLine.GetComponent<LineRenderer>();
    		edgeCollider = currentLine.GetComponent<EdgeCollider2D>();
    		fingerPositions.Clear();
    		// Como lineRenderer es una línea, necesitamos añadir dos puntos a fingerPositions para poder dibujarla sin errores
    		fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
    		fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
    		// Dibujamos una línea compuesta de dos puntos
    		lineRenderer.SetPosition(0, fingerPositions[0]);
    		lineRenderer.SetPosition(1, fingerPositions[1]);
    		edgeCollider.points = fingerPositions.ToArray();
    	}
    
    	void UpdateLine(Vector2 newFingerPos){
    		fingerPositions.Add(newFingerPos);
    		lineRenderer.positionCount++;
    		// Convertimos el List de posiciones por las que ha ido pasando el dedo en la línea que vamos a ver
    		lineRenderer.SetPosition(lineRenderer.positionCount - 1, newFingerPos);
    		// Convertimos el List de posiciones por las que ha ido pasando el dedo en los puntos del edge collider
    		edgeCollider.points = fingerPositions.ToArray();
    	}
    }

    Detección de colisión

    Player.cs[RequireComponent (typeof (Controller2D))]
    public class Player : MonoBehaviour{
    	float gravity = -20;
    	Vector3 velocity;
    	Controller2D controller;
    
    	void Start(){
    		controller = GetComponent<Controller2D>();
    	}
    
    	void Update(){
    		CalculateVelocity();	
    		controller.Move(velocity*Time.deltaTime);
    	}
    	void CalculateVelocity() {
    		velocity.y += gravity * Time.deltaTime;
    	}
    }

    A los objetos con los que deseamos colisionar debemos asignarles el layer Obstacle.

    Debemos asignar al collisionMask del player que colisione con los objetos de la layer Obstacle.

    uso de layers

    Movimiento horizontal

    Moveremos al player con las teclas a y d. Si usamos las flechas del teclado para mover al player podemos tener conflictos ya que las flechas tienen funciones dentro de Unity.

    Controller2D.cspublic void Move(Vector3 velocity){
    	transform.Translate(velocity);
    }
    Controller2D.cspublic class Controller2D : RaycastController{
    	public void Move(Vector3 velocity){
    		UpdateRaycastOrigins();
    		VerticalCollisions();
    		transform.Translate(velocity);
    	}
    
    	public void VerticalCollisions(){
    		for (int i = 0; i < verticalRayCount; i++){
    			Vector2 rayOrigin = raycastOrigins.bottomLeft;
    			rayOrigin += Vector2.right * verticalRaySpacing * i;
    			Debug.DrawRay(rayOrigin, Vector2.up * -2, Color.red);
    		}
    	}
    }
    

    Lanzamos los rayos un poco adelante en la dirección en la que el player se está moviendo

    Vamos a lanzar el rayo en la posición en la que estaremos un momento después. Esto nos permitirá anticipar las colisiones en la dirección del movimiento. Primero evaluaremos si habrá colisión y después ejecutaremos el movimiento.

    RaycastController.csusing System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class RaycastController : MonoBehaviour{
    	const float dstBetweenRays = 0.25f;
    	[HideInInspector]
    	public int verticalRayCount = 4; // lanzaremos 4 rayos hacia abajo. Esto se podrá cambiar desde el inspector
    	[HideInInspector]
    	public float verticalRaySpacing = 1;
    	public BoxCollider2D collider;
    	// raycastOrigins es una estructura que contiene las coordenadas de cada una de las cuatro esquinas del cubo
    	public RaycastOrigins raycastOrigins;
    
    	public virtual void Start(){
    		collider = GetComponent<BoxCollider2D>();
    		CalculateRaySpacing();
    	}
        
    	public void UpdateRaycastOrigins(){
    		Bounds bounds = collider.bounds;
    		raycastOrigins.bottomLeft = new Vector2(bounds.min.x, bounds.min.y);
    		raycastOrigins.bottomRight = new Vector2(bounds.max.x, bounds.min.y);
    		raycastOrigins.topLeft = new Vector2(bounds.min.x, bounds.max.y);
    		raycastOrigins.topRight = new Vector2(bounds.max.x, bounds.max.y);
    	}
    
    	public void CalculateRaySpacing(){
    		Bounds bounds = collider.bounds;
    		float boundsWidth = bounds.size.x;
    		verticalRayCount = Mathf.RoundToInt(boundsWidth / dstBetweenRays);
    		verticalRaySpacing = bounds.size.x / (verticalRayCount - 1);
    	}
    
    	public struct RaycastOrigins{
    		public Vector2  topLeft, topRight, bottomLeft, bottomRight;
    	}
    }

    Colisión horizontal

    RaycastController.cspublic const float skinWidth = .015f;
    ...
    void UpdateRaycastOrigins(){
    	Bounds bounds = collider.bounds;
    	bounds.Expand(skinWidth * -2);
    	...
    void CalculateRaySpacing(){
    	Bounds bounds = collider.bounds;
    	bounds.Expand(skinWidth * -2);
    	...
    Controller2D.cspublic LayerMask collisionMask;
    
    public void Move(Vector3 velocity){
    	UpdateRaycastOrigins();
    	VerticalCollisions(ref velocity);
    	transform.Translate(velocity);
    }
    
    void VerticalCollisions(ref Vector3 velocity){
    	float directionY = Mathf.Sign(velocity.y);
    	float rayLength = Mathf.Abs(velocity.y) + skinWidth;
    	for (int i = 0; i < verticalRayCount; i++){
    		...
    		RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
    		if (hit){
    			velocity.y = (hit.distance - skinWidth) * directionY;
    			rayLength = hit.distance;
    		}
    	}
    }

    Gestionar colisiones sólo cuando el player se mueva

    PlayerInput.cs (no te olvides de vincular esta nueva clase al Player)[RequireComponent(typeof(Player))]
    public class PlayerInput : MonoBehaviour{
    	Player player;
    	void Start(){
    		player = GetComponent<Player>();
    	}
    	void Update(){
    		Vector2 directionalInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
    		player.SetDirectionalInput(directionalInput);
    	}
    }

    No acumular gravedad mientras estamos en una plataforma

    Antes de ejecutar este paso, veremos que si despues de caer sobre la plataforma salimos de la misma, el player bajará muy bruscamente hacia abajo. Para evitarlo, dejaremos de acumular velocidad cuando el Player esté sobre una plataforma.

    Después de ejecutar este paso, el player caerá muy lentamente. Para corregir esto, ejecutaremos el paso siguiente.

    Player.csfloat moveSpeed = 6;
    Vector2 directionalInput;
    
    void CalculateVelocity(){
    	velocity.x = directionalInput.x * moveSpeed;
    	...
    }
    
    public void SetDirectionalInput(Vector2 input){
    	directionalInput = input;
    }
    Controller2D.csvoid VerticalCollisions(ref Vector3 velocity){
    	...
    	rayOrigin += Vector2.right * verticalRaySpacing * i;
    	rayOrigin += Vector2.right * (verticalRaySpacing * i + velocity.x);
    	...
    }

    Resetear collisionInfo

    Si no volvemos a poner a false las variables above, below, left y right, continuarán indefinidamente valiendo true después de una colisión y por tanto la velocity.y del cubo se mantendrá a 0.

    RaycastController.cs[HideInInspector]
    public int horizontalRayCount = 4;
    [HideInInspector]
    public float horizontalRaySpacing;
    
    ...
    void CalculateRaySpacing(){
    	...
    	horizontalRayCount = Mathf.Clamp(horizontalRayCount, 2, int.MaxValue);
    	horizontalRaySpacing = bounds.size.y / (horizontalRayCount - 1);
    }

    Saltar

    Controller2D.cspublic void Move(Vector3 velocity){
    	UpdateRaycastOrigins();
    	HorizontalCollisions(ref velocity);
    	VerticalCollisions(ref velocity);
    	transform.Translate(velocity);
    }
    void HorizontalCollisions(ref Vector3 velocity){
    	float directionX = Mathf.Sign(velocity.x);
    	float rayLength = Mathf.Abs(velocity.x) + skinWidth;
    	for (int i = 0; i < horizontalRayCount; i++){
    		Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomLeft : raycastOrigins.bottomRight;
    		rayOrigin += Vector2.up * horizontalRaySpacing * i;
    		RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
    		Debug.DrawRay(rayOrigin , Vector2.right * directionX * rayLength, Color.blue);
    		if (hit){
    			velocity.x = (hit.distance - skinWidth) * directionX;
    			rayLength = hit.distance;
    		}
    	}
    }
    Controller2D.cspublic void Move(Vector3 velocity){
    	UpdateRaycastOrigins();
    	HorizontalCollisions(ref velocity);
    	VerticalCollisions(ref velocity);
    	if (velocity.x != 0) {
    		HorizontalCollisions(ref velocity);
    	}
    
    	if(velocity.y != 0){
    		VerticalCollisions(ref velocity);
    	}
    	transform.Translate(velocity);
    }

    Transformar jumpHeight y timeToJumpApex en gravity y velocity

    jumpHeight y timeToJumpApex serán unidades más útiles para trabajar.

    Controler2D.cspublic CollisionInfo collisions;
    
    void VerticalCollisions(ref Vector3 velocity){
    	...
    		if (hit){
    			velocity.y = (hit.distance - skinWidth) * directionY;
    			rayLength = hit.distance;
    			collisions.below = directionY == -1; // Si está llendo hacia arriba, y colisiona, below valdrá true
    			collisions.above = directionY == 1; // Si está llendo hacia abajo, y colisiona, above valdrá true
    		}
    	}
    }
    
    void HorizontalCollisions(ref Vector3 velocity){
    	...
    		if (hit){
    			velocity.x = (hit.distance - skinWidth) * directionX;
    			rayLength = hit.distance;
    			collisions.left = directionX == -1; // Si está llendo hacia la izquierda, y colisiona, left valdrá true
    			collisions.right = directionX == 1; // Si está llendo hacia la derecha, y colisiona, right valdrá true
    		}
    	}
    }
    
    public struct CollisionInfo{
    	public bool above, below;
    	public bool left, right;
    }

    Deceleración progresiva en los cambios de dirección

    Player.csvoid Update(){
    	CalculateVelocity();
    	controller.Move(velocity * Time.deltaTime);
    
    	if (controller.collisions.above || controller.collisions.below){
    		velocity.y = 0;
    	}
    }

    Subiendo cuestas

    Con el código que tenemos hasta ahora, el player subirá las cuestas muy lentamente. Vamos a adaptar nuestro código para que la subida se realice a la misma velocidad que cuando el player avanza horizontalmente.

    Controller2D.cspublic void Move(Vector3 velocity){
    	UpdateRaycastOrigins();
    	collisions.Reset();
    	...
    }
    
    public struct CollisionInfo{
    	...
    
    	public void Reset(){
    		above = below = false;
    		left = right = false;
    	}
    }

    Corregir la imposibilidad de saltar mientras subimos una cuesta

    El siguiente código nos permitirá saltar en una cuesta, pero sólo cuando estamos subiendo.

    Player.csfloat jumpVelocity = 8;
    public void OnJumpInputDown(){
    	if (controller.collisions.below){
    	velocity.y = jumpVelocity;
    	}
    }

    Optimizamos el código para que la gestión de la colisión con la rampa sólo se ejecute cuando sea necesario

    Ahora mismo, cuando el player choca de frente contra el inicio de una rampa, puede pasar que se quede trabado y no la suba. Esto se solucionará en los dos siguientes pasos (corrección del tembleque al colisionar de frente cuando estamos subiendo una rampa y corrección del tembleque al colisionar por arriba.)

    PlayerInput.csUpdate(){
    	...
    	if (Input.GetKeyDown(KeyCode.Space)){
    		player.OnJumpInputDown();
    	}
    }

    Corrección del tembleque al colisionar de frente cuando estamos subiendo una rampa

    Esto ocurre sólo cuando el alto de la pared contra la que colisionamos no abarca toda la altura del player.

    colisión del tembleque al colisionar de frente
    Player.cspublic float jumpHeight = 4;
    public float timeToJumpApex = .4f;
    
    void Start(){
    	controller = GetComponent<Controller2D>();
    
    	gravity = -(2 * jumpHeight) / Mathf.Pow(timeToJumpApex, 2);
    	jumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
    }

    Corrección del tembleque al colisionar por arriba

    problema al colisionar contra un techo
    Player.csfloat velocityXSmoothing;
    float accelerationTimeAirborne = .2f;
    float accelerationTimeGrounded = .1f;
    
    
    void CalculateVelocity(){
    	float targetVelocityX = directionalInput.x * moveSpeed;
    	velocity.x = Mathf.SmoothDamp(velocity.x, targetVelocityX, ref velocityXSmoothing, (controller.collisions.below)?accelerationTimeGrounded: accelerationTimeAirborne);
    	velocity.x = directionalInput.x * moveSpeed;
    	velocity.y += gravity * Time.deltaTime;
    }

    Bajando cuestas

    Cuando la cuesta sea bastante pronunciada, hasta ahora, el cubo estaba bajando dando saltitos. Para corregirlo, utilizaremos el siguiente código.

    Controller2D.csfloat maxClimbAngle = 80;
    
    void HorizontalCollisions(ref Vector3 velocity){
    	...
    		if (hit){
    			float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
    			if(i == 0 && slopeAngle <= maxClimbAngle){
    				ClimbSlope(ref velocity, slopeAngle);
    			}
    			...
    		}
    	}
    }
    
    void ClimbSlope(ref Vector3 velocity, float slopeAngle){
    	float moveDistance = Mathf.Abs(velocity.x);
    	velocity.y = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
    	velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x); 
    }

    Corrección de la pequeña parada que hace el player al converger dos rampas de subida y bajada

    Esto no ocurre en todos los casos, pero sí en algunos.

    rampa de subida y bajada
    void ClimbSlope(ref Vector3 velocity, float slopeAngle){
    	float moveDistance = Mathf.Abs(velocity.x);
    	float climbVelocityY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
    	if(velocity.y <= climbVelocityY){
    		velocity.y = climbVelocityY;
    		velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x);
    		collisions.below = true;
    	}
    }

    Plataformas móviles

    • Cuando la plataforma se mueve en horizontal, el Player se quedará quieto mientras la plataforma avanza. Este será un problema que arreglemos en el siguiente paso.
    • La plataforma se moverá en una única dirección hasta salir por los límites de la pantalla.
    Controller2D.csvoid HorizontalCollisions(ref Vector3 velocity){
    	...
    	if (hit){
    		...
    		if(!collisions.climbingSlope || slopeAngle > maxClimbAngle){
    			velocity.x = (hit.distance - skinWidth) * directionX;
    			rayLength = hit.distance;
    
    			collisions.left = directionX == -1;
    			collisions.right = directionX == 1;
    		}
    	}
    }
    
    void ClimbSlope(ref Vector3 velocity, float slopeAngle){
    	...
    	if(velocity.y <= climbVelocityY){
    		...
    		collisions.climbingSlope = true;
    		collisions.slopeAngle = slopeAngle;
    	}
    }
    
    public struct CollisionInfo{
    	public bool above, below;
    	public bool left, right;
    	public bool climbingSlope;
    	public float slopeAngle, slopeAngleOld;
    	public void Reset(){
    		above = below = false;
    		left = right = false;
    		climbingSlope = false;
    		slopeAngleOld = slopeAngle;
    	}
    }

    Habrá que darle valor al Vector3 en el inspector.

    Mover correctamente al player sobre la plataforma

    • De momento sólo estamos controlando el movimiento del player sobre la plataforma cuando esta se mueve verticalmente.
    • Debes asegurarte de que la plataforma tiene rayos suficientes como para colisionar contra el Player.
    • El valor de la propiedad passengerMask será player. Para que funcione, asignaremos el layer player al objeto Player.
    void HorizontalCollisions(ref Vector3 velocity){
    	...
    	if (!collisions.climbingSlope || slopeAngle > maxClimbAngle) {
    		velocity.x = (hit.distance - skinWidth) * directionX;
    		rayLength = hit.distance;
    
    		if (collisions.climbingSlope){
    			velocity.y = Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x);
    		}
    		collisions.left = directionX == -1;
    		collisions.right = directionX == 1;
    	}
    	...
    }

    Gestionar movimiento del player cuando la plataforma se mueve horizontalmente

    Cuando implementemos el siguiente código, todavía tendremos errores, ya que la plataforma resbalará por debajo del player. Esto será algo que corregiremos en el siguiente paso.

    Controller2D.csvoid VerticalCollisions(ref Vector3 velocity){
    	...
    	for (int i = 0; i < verticalRayCount; i++){
    		Vector2 rayOrigin =  raycastOrigins.bottomLeft;
    		Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
    		//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
    		...
    
    		if (hit){
    			velocity.y = (hit.distance - skinWidth) * directionY;
    			rayLength = hit.distance;
    
    			if (collisions.climbingSlope){
    				velocity.x = velocity.y / Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Sign(velocity.x);
    			}
    
    			collisions.below = directionY == -1;
    			collisions.above = directionY == 1;
    		}
    	}
    }

    Corregir errores del movimiento horizontal y hacia abajo de la plataforma

    Controller2D.csfloat maxDescendAngle = 80;
    
    public void Move(Vector3 velocity){
    	UpdateRaycastOrigins();
    	collisions.Reset();
    	if(velocity.y < 0){
    		DescendSlope(ref velocity);
    	}
    	...
    
    void DescendSlope(ref Vector3 velocity){
    	float directionX = Mathf.Sign(velocity.x);
    	Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomRight : raycastOrigins.bottomLeft;
    	RaycastHit2D hit = Physics2D.Raycast(rayOrigin, -Vector2.up, Mathf.Infinity, collisionMask);
    	if (hit){
    		float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
    		if(slopeAngle != 0 && slopeAngle <= maxDescendAngle){
    			if(Mathf.Sign(hit.normal.x) == directionX){
    				if(hit.distance - skinWidth <= Mathf.Tan(slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x)){
    					float moveDistance = Mathf.Abs(velocity.x);
    					float descendVelocityY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
    					velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x);
    					velocity.y -= descendVelocityY;
    					collisions.slopeAngle = slopeAngle;
    					collisions.descendingSlope = true;
    					collisions.below = true;
    				}
    			}
    		}
    	}
    }
    
    public struct CollisionInfo{
    	...
    	public bool descendingSlope;
    	...
    	public void Reset(){
    		...
    		descendingSlope = false;
    		slopeAngle = 0;
    	}
    }

    Gestionar la colisión de la plataforma con paredes

    Renombramos la función MovePassengers como CalculatePassengerMovement y creamos la función MovePassenger.

    Controller2D.cspublic void Move(Vector3 velocity) {
    	UpdateRaycastOrigins();
    	collisions.Reset();
    	collisions.velocityOld = velocity;
    	...
    }
    
    void HorizontalCollisions(ref Vector3 velocity){
    	...
    	if (i == 0 && slopeAngle <= maxClimbAngle) {
    		if (collisions.descendingSlope){
    			collisions.descendingSlope = false;
    			velocity = collisions.velocityOld;
    		}
    		ClimbSlope(ref velocity, slopeAngle);
    	}
    	...
    }
    
    public struct CollisionInfo{
    	public Vector3 velocityOld;
    	...
    }

    Reducir el número de llamadas al método GetComponent

    Añadimos a un diccionario el componente Controller2D del player, para en lo sucesivo no tener que llamar al método GetComponent.

    PlatformController.cspublic class PlatformController : RaycastController{
    	public Vector3 move;
    
    	public override void Start(){
    		base.Start();
    	}
    
    	void Update(){
    		UpdateRaycastOrigins();
    		Vector3 velocity = move * Time.deltaTime;
    		transform.Translate(velocity);
        }
    }

    Corregir algunos problemas del player sobre la plataforma

    • A veces el player no puede saltar cuando la plataforma se desplaza verticalmente
    • Cuando la plataforma aterriza sobre el player este se desplaza horizontalmente.
    PlatformController.cspublic LayerMask passengerMask;
    void Update(){
    	...
    	MovePassengers(velocity);
    }
    
    
    void MovePassengers(Vector3 velocity){
    	HashSet<Transform> movedPassengers = new HashSet<Transform>();
    	float directionX = Mathf.Sign(velocity.x);
    	float directionY = Mathf.Sign(velocity.y);
    
    	if (velocity.y != 0){
    		float rayLength = Mathf.Abs(velocity.y) + skinWidth;
    		for (int i = 0; i < verticalRayCount; i++){
    			//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
    			Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
    			rayOrigin += Vector2.right * (verticalRaySpacing * i);
    			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, passengerMask);
    			Debug.DrawRay(rayOrigin, Vector2.up * directionY, Color.blue); 
    
    			if (hit){
    				if (!movedPassengers.Contains(hit.transform)){
    					movedPassengers.Add(hit.transform);
    					float pushX = (directionY == 1) ? velocity.x : 0;
    					float pushY = velocity.y - (hit.distance - skinWidth) * directionY;
    					hit.transform.Translate(new Vector3(pushX, pushY));
    				}
    			}
    		}
    	}
    }

    Actualizamos el método move con el siguiente código:

    PlatformController.csvoid MovePassengers(Vector3 velocity){
    	...	
    	if (velocity.x != 0) {
    		float rayLength = Mathf.Abs (velocity.x) + skinWidth;
    			
    		for (int i = 0; i < horizontalRayCount; i ++) {
    			Vector2 rayOrigin = (directionX == -1)?raycastOrigins.bottomLeft:raycastOrigins.bottomRight;
    			rayOrigin += Vector2.up * (horizontalRaySpacing * i);
    			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, passengerMask);
    
    			if (hit) {
    				if (!movedPassengers.Contains(hit.transform)) {
    					movedPassengers.Add(hit.transform);
    					float pushX = velocity.x - (hit.distance - skinWidth) * directionX;
    					float pushY = -skinWidth;
    						
    					hit.transform.Translate(new Vector3(pushX,pushY));
    				}
    			}
    		}
    	}
    }

    Solucionar que cuando la plataforma desciende superponiéndose sobre el player mientras este se mueve horizontalmente, el player deja de responder correctamente a los controles horizontales de movimiento

    Cuando el cubo se superpone sobre la plataforma, su distancia a la misma será cero. En estos casos, queremos que igualmente, la plataforma se pueda mover correctamente horizontalmente, por eso hacemos un continue, para no tener en cuenta ese rayo.

    Si el cubo lanza sus rayos contra una pared, el rayo sí que será tenido en cuenta, ya que su distancia no será cero, por tener un skinwidth de separación.

    PlatformController.csvoid MovePassengers(Vector3 velocity){
    	...
    	// Si el pasajero está encima de una plataforma que se mueve hacia abajo u horizontalmente
    	if (directionY == -1 || velocity.y == 0 && velocity.x != 0){
    		float rayLength = skinWidth * 2;
    		for (int i = 0; i < verticalRayCount; i++){
    			//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
    			Vector2 rayOrigin = raycastOrigins.topLeft + Vector2.right * (verticalRaySpacing * i);
    			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up, rayLength, passengerMask);
    			if (hit){
    				if (!movedPassengers.Contains(hit.transform)){
    					movedPassengers.Add(hit.transform);
    					float pushX = velocity.x;
    					float pushY = velocity.y;
    					hit.transform.Translate(new Vector3(pushX, pushY));
    				}
    			}
    		}
    	}
    }

    Definiendo waypoints para las plataformas

    Una vez añadido el código, en el panel inspector definiremos los puntos entre los que se moverán las plataformas.

    De momento sólo definiremos los puntos. La plataforma no se moverá entre ellos.

    PlatformController.csList<PassengerMovement> passengerMovement;
    
    void Update(){
    	UpdateRaycastOrigins();
    	Vector3 velocity = move * Time.deltaTime;
    		
    	CalculatePassengerMovement(velocity);
    	MovePassengers(velocity);
    	MovePassengers(true);
    	transform.Translate(velocity);
    	MovePassengers(false);
    }
    
    void MovePassengers(bool beforeMovePlatform){
    	foreach(PassengerMovement passenger in passengerMovement){
    		if(passenger.moveBeforePlatform == beforeMovePlatform){
    			passenger.transform.GetComponent<Controller2D>().Move(passenger.velocity);
    		}
    	}
    }
    
    void CalculatePassengerMovement(Vector3 velocity){
    	passengerMovement = new List<PassengerMovement>();
    	...
    	if(velocity.y != 0){
    		...
    		if (!movedPassengers.Contains(hit.transform)){
    			...
    			hit.transform.Translate(new Vector3(pushX, pushY));
    			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), directionY==1, true));
    		}
    	...
    				}
    	if (velocity.x != 0){
    		...
    		if (!movedPassengers.Contains(hit.transform)){
    			...
    			hit.transform.Translate(new Vector3(pushX, pushY));
    			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), false, true));
    		}
    		...
    	}	
    	if(directionY == -1 || velocity.y == 0 && velocity.x != 0){
    		...
    		if (!movedPassengers.Contains(hit.transform)){
    			...
    			hit.transform.Translate(new Vector3(pushX, pushY));
    			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), true, false));
    		}
    		...
    	}
    }
    
    struct PassengerMovement {
    	public Transform transform;
    	public Vector3 velocity;
    	public bool standingOnPlatform;
    	public bool moveBeforePlatform;
    
    	public PassengerMovement(Transform _transform, Vector3 _velocity, bool _standingOnPlatform, bool _moveBeforePlatform) {
    		transform = _transform;
    		velocity = _velocity;
    		standingOnPlatform = _standingOnPlatform;
    		moveBeforePlatform = _moveBeforePlatform;
    	}
    }

    Definimos dos wayPonts; uno con coordenadas (0,0,0) y otro en el lugar al que queremos que se mueva la plataforma.

    Evitar que la posición de destino del gizmo cambie

    Para probar este punto tendremos que definir valores en el inspector para:

    • Local Waypoints (al menos dos puntos)
    • speed
    PlatformController.csDictionary<Transform, Controller2D> passengerDictionary = new Dictionary<Transform, Controller2D>();
    void MovePassengers(bool beforeMovePlatform){
    	foreach(PassengerMovement passenger in passengerMovement){
    		if (!passengerDictionary.ContainsKey(passenger.transform)){
    			passengerDictionary.Add(passenger.transform, passenger.transform.GetComponent<Controller2D>());
    		}
    
    		if(passenger.moveBeforePlatform == beforeMovePlatform){
    			passenger.transform.GetComponent<Controller2D>().Move(passenger.velocity);
    			passengerDictionary[passenger.transform].Move(passenger.velocity);
    		}
    	}
    }

    La plataforma se mueve cíclicamente

    Vamos a poder seleccionar si la plataforma volverá a su posición inicial recorriendo el camino inverso o llendo directamente desde el punto final al inicial.

    Fíjate que tendremos que asignar una speed a la plataforma.

    PlatformController.csvoid MovePassengers(bool beforeMovePlatform){
    	foreach(PassengerMovement passenger in passengerMovement){
    		...
    		if (passenger.moveBeforePlatform == beforeMovePlatform){
    			passengerDictionary[passenger.transform].Move(passenger.velocity);
    			passengerDictionary[passenger.transform].Move(passenger.velocity,passenger.standingOnPlatform);
    		}
    	}

    waitTime

    waitTime será el tiempo que la plataforma se detiene en una posición concreta.

    Controller2D.cspublic void Move(Vector3 velocity, bool standingOnPlatform = false ){
    	...
    	if( standingOnPlatform){
    		collisions.below = true;
    	}
    }

    Aceleración del movimiento

    Controller2D.csvoid HorizontalCollisions(ref Vector3 velocity){
    	...
    	if (hit){
    		if(hit.distance == 0){
    			continue;
    		}
    		...

    Wall jumping

    Caer derrapando contra una pared vertical

    PlatformController.cspublic Vector3[] localWaypoints;
    
    void OnDrawGizmos(){
    	if(localWaypoints != null){
    		Gizmos.color = Color.red;
    		float size = .3f;
    
    		for (int i = 0; i<localWaypoints.Length; i++) {
    			Vector3 globalWaypointsPos = localWaypoints[i] + transform.position;
    			Gizmos.DrawLine(globalWaypointsPos - Vector3.up * size, globalWaypointsPos + Vector3.up * size);
    			Gizmos.DrawLine(globalWaypointsPos - Vector3.left * size, globalWaypointsPos + Vector3.left * size);
    		}
    	}
    }

    Solucionar que las colisiones sólo son detectadas si nos movemos contra la pared

    PlatformController.csVector3[] globalWaypoints;
    public float speed;
    int fromWaypointIndex;
    float percentBetweenWaypoints;
    
    public override void Start(){
    	base.Start();
    	globalWaypoints = new Vector3[localWaypoints.Length];
    	for (int i =0; i < localWaypoints.Length; i++) {
    		globalWaypoints[i] = localWaypoints[i] + transform.position;
    	}
    }
    
    void OnDrawGizmos(){
    	...
    	for (int i = 0; i<localWaypoints.Length; i++) {
    		Vector3 globalWaypointsPos = localWaypoints[i] + transform.position;
    		Vector3 globalWaypointsPos = (Application.isPlaying) ? globalWaypoints[i] : localWaypoints[i] + transform.position;
    		...
    
    
    void Update(){
    	...
    	Vector3 velocity = move * Time.deltaTime;
    	Vector3 velocity = CalculatePlatformMovement(); //Si no pones esta línea la plataforma no se moverá y no dará error
    	...
    }
    Vector3 CalculatePlatformMovement(){
    	int toWaypointIndex = fromWaypointIndex + 1;
    	float distanceBetweenWaypoints = Vector3.Distance(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex]);
    	percentBetweenWaypoints += Time.deltaTime * speed/distanceBetweenWaypoints;
    	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], percentBetweenWaypoints);
    
    	if(percentBetweenWaypoints >= 1){
    		percentBetweenWaypoints = 0;
    		fromWaypointIndex++;
    		if(fromWaypointIndex >= globalWaypoints.Length - 1){
    			fromWaypointIndex = 0;
    			System.Array.Reverse(globalWaypoints);
    		}
    	}
    	return newPos - transform.position;
    }

    Llegados a este punto es indispensable que no te hayas olvidado de dar este paso:

    En Edit -> Project Settings -> Physics 2D ...

    • Desmarcamos la check Reuse Collision Callbacks
    • Marcamos la check Auto Sync Transforms
    Configuración rayos en Unity

    Añadimos la posibilidad de saltar mientras nos apoyamos contra la pared

    Tendremos que dar valores a las variables de salto en el inspector:

    • Wall Jump Off(fuerza de salto cuando estamos apoyados en una pared y no tocamos las teclas de dirección): X: 7.5, Y:16
    • Wall Jump Climb (fuerza de salto cuando estamos apoyados en una pared y tocamos la tecla para ir en la dirección de la pared): X: 8.5, Y: 7
    • Wall Leap (fuerza de salto cuando estamos apoyados en una pared y tocamos la tecla para ir en la dirección contraria de la pared): X:18, Y: 17
    PlatformController.cspublic bool cyclic;
    Vector3 CalculatePlatformMovement(){
    	int toWaypointIndex = fromWaypointIndex + 1;
    	fromWaypointIndex %= globalWaypoints.Length;
    	int toWaypointIndex = (fromWaypointIndex + 1) % globalWaypoints.Length;
    	...
    	if(percentBetweenWaypoints >= 1){
    		percentBetweenWaypoints = 0;
    		fromWaypointIndex++;
    		if(!cyclic){
    			if(fromWaypointIndex >= globalWaypoints.Length - 1){
    				fromWaypointIndex = 0;
    				System.Array.Reverse(globalWaypoints);
    			}
    		}
    	}
    
    

    Salto a varias alturas

    PlatformController.cspublic float waitTime;
    float nextMoveTime;
    
    Vector3 CalculatePlatformMovement(){
    	if(Time.time < nextMoveTime){
    		return Vector3.zero;
    	}
    	...
    	if(percentBetweenWaypoints >= 1){
    		...
    		nextMoveTime = Time.time + waitTime;
    	}
    }
    PlatformController.cs[Range(0,2)]
    public float easeAmount;
    
    float Ease(float x){
    	float a = easeAmount +1;
    	return Mathf.Pow(x,a) / (Mathf.Pow(x,a) + Mathf.Pow(1-x, a));
    }
    
    Vector3 CalculatePlatformMovement(){
    	fromWaypointIndex %= globalWaypoints.Length;
    	int toWaypointIndex = (fromWaypointIndex + 1) % globalWaypoints.Length;
    	float distanceBetweenWaypoints = Vector3.Distance(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex]);
    	percentBetweenWaypoints += Time.deltaTime * speed / distanceBetweenWaypoints;
    	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], percentBetweenWaypoints);
    
    	percentBetweenWaypoints = Mathf.Clamp01(percentBetweenWaypoints);
    	float easedPercentBetweenWaypoints = Ease(percentBetweenWaypoints);  
    	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], easedPercentBetweenWaypoints);

    Saltar a una plataforma desde abajo

    Creamos una plataforma con el tag Through y el layer Obstacle.

    Player.csbool wallSliding;
    public float wallSlideSpeedMax = 3;
    
    void Update(){
    	...
    	HandleWallSliding();
    }
    
    void HandleWallSliding(){
    	wallSliding = false;
    	if ((controller.collisions.left || controller.collisions.right) && !controller.collisions.below && velocity.y < 0){
    		wallSliding = true;
    		if (velocity.y < -wallSlideSpeedMax){
    			velocity.y = -wallSlideSpeedMax;
    		}
    	}
    }

    Corregir el problema de que si no llegamos a la parte superior de la plataforma, igualmente subimos sobre ella

    Controller2D.cspublic override void Start(){
    	base.Start();
    	collisions.faceDir = 1;
    }
    
    public void Move(Vector3 velocity, bool standingOnPlatform = false ){
    	...
    	if (velocity.x != 0){
    		HorizontalCollisions(ref velocity);
    		collisions.faceDir = (int)Mathf.Sign(velocity.x);
    	}
    	...	
    	HorizontalCollisions(ref velocity);
    	
    public struct CollisionInfo{
    	public float faceDir;
    	...					
    }
    
    void HorizontalCollisions(ref Vector3 velocity){
    	float directionX = Mathf.Sign(velocity.x);
    	float directionX = collisions.faceDir;
    	float rayLength = Mathf.Abs(velocity.x) + skinWidth;
    	if (Mathf.Abs(velocity.x) < skinWidth){
    		rayLength = 2 * skinWidth;
    	}
    	...
    }

    Caer de la plataforma al pulsar abajo

    Será necesario que la plataforma esté etiquetada como "Through".

    Player.csint wallDirX;
    
    float maxJumpVelocity = 20;
    float minJumpVelocity = 10;
    
    public Vector2 wallJumpOff;
    public Vector2 wallJumpClimb;
    public Vector2 wallLeap;
    
    public void OnJumpInputDown(){
    	if (controller.collisions.below){
    		velocity.y = jumpVelocity;
    	}
    }
    
    public void OnJumpInputDown(){
    	if (wallSliding){
    		if (wallDirX == directionalInput.x){
    			velocity.x = -wallDirX * wallJumpClimb.x;
    			velocity.y = wallJumpClimb.y;
    		}else if (directionalInput.x == 0){
    			velocity.x = -wallDirX * wallJumpOff.x;
    			velocity.y = wallJumpOff.y;
    		}else{
    			velocity.x = -wallDirX * wallLeap.x;
    			velocity.y = wallLeap.y;
    		}
    	}
    	if (controller.collisions.below){
    		velocity.y = maxJumpVelocity;
    	}
    }
    
    void HandleWallSliding(){
    	wallDirX = (controller.collisions.left) ? -1 : 1;
    	...
    }
    Player.cspublic float jumpHeight;
    public float maxJumpHeight = 4;
    public float minJumpHeight = 1;
    float jumpVelocity;
    
    void Start(){
    	...
    	gravity = -(2 * jumpHeight) / Mathf.Pow(timeToJumpApex, 2);
    	jumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
    	gravity = -(2 * jumpHeight) / Mathf.Pow (timeToJumpApex, 2);
    	gravity = -(2 * maxJumpHeight) / Mathf.Pow(timeToJumpApex, 2);
    
    	maxJumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
    	minJumpVelocity = Mathf.Sqrt(2 * Mathf.Abs(gravity) * minJumpHeight);
    }
    
    public void OnJumpInputUp() {
    	if (velocity.y > minJumpVelocity) {
    		velocity.y = minJumpVelocity;
    	}
    }

    La cámara

    La focus area sigue al player

    El siguiente script estará vinculado a la cámara del juego. A su propiedad target le daremos le asignaremos al player.

    Cuando probemos nuestro código, deberemos definir un tamaño para la focus area.

    PlayerInput.csvoid Update(){
    	...
    	if (Input.GetKeyUp (KeyCode.Space)) {
    		player.OnJumpInputUp ();
    	}
    }

    Mejorar el efecto de la cámara

    Vamos a hacer que el player no siempre esté en el centro de la cámara.

    Controller2D.csvoid VerticalCollisions(ref Vector3 velocity){
    	if (hit){
    		if (hit.collider.tag == "Through"){
    			if (directionY == 1){
    				continue;
    			}
    		}
    	...

    Algunas mejoras

    Controller2D.csif (directionY == 1 || hit.distance == 0)

    Mover la cámara a ambos lados del player

    Controller2D.cspublic Vector2 playerInput;
    
    public void Move (Vector3 velocity, bool standingOnPlatform){
    	Move(velocity, Vector2.zero, standingOnPlatform);
    }
    
    public void Move(Vector3 velocity, Vector2 input, bool standingOnPlatform = false){
    	UpdateRaycastOrigins();
    	collisions.Reset();
    	collisions.velocityOld = velocity;
    	playerInput = input;
    	...
    }
    
    void VerticalCollisions(ref Vector3 velocity){aaaa
    	...
    	if (hit){
    		if (hit.collider.tag == "Through"){
    			if (directionY == 1 || hit.distance == 0){
    				continue;
    			}
    			if (playerInput.y == -1){
    				continue;
    			}
    

    Mejorar el posicionamiento de la focus area

    Realmente, sólo queremos cambiar la posición de la focus area si el jugador mira y camina en cierta dirección, no sólo si camina en esa dirección.

    Player.csvoid Update(){
    	...
    	//controller.Move(velocity * Time.deltaTime);
    	controller.Move(velocity * Time.deltaTime, directionalInput);
    	...
    }

    Mover realmente la cámara

    public class CameraFollow : MonoBehaviour{
    	public Controller2D target;
    	public Vector2 focusAreaSize;
    
    	FocusArea focusArea;
    
    	void Start(){
    		focusArea = new FocusArea(target.collider.bounds, focusAreaSize);
    	}
    
    	void LateUpdate(){
    		focusArea.Update(target.collider.bounds);
    	}
    
    	void OnDrawGizmos(){
    		Gizmos.color = new Color(1, 0, 0, .5f);
    		Gizmos.DrawCube(focusArea.centre, focusAreaSize);
    	}
        
    	struct FocusArea{
    		public Vector2 centre;
    		public Vector2 velocity;
    
    		float left, right;
    		float top, bottom;
    
    		public FocusArea(Bounds targetBounds, Vector2 size){
    			left = targetBounds.center.x - size.x / 2;
    			right = targetBounds.center.x + size.x / 2;
    			bottom = targetBounds.min.y;
    			top = targetBounds.min.y + size.y;
    
    			velocity = Vector2.zero;
    			centre = new Vector2((left + right) / 2, (top + bottom) /2); 
    		}
    
    		public void Update(Bounds targetBounds){
    			float shiftX = 0;
    			if(targetBounds.min.x < left){
    				shiftX = targetBounds.min.x - left;
    			}else if (targetBounds.max.x > right){
    				shiftX = targetBounds.max.x - right;
    			}
    			left += shiftX;
    			right += shiftX;
    
    			float shiftY = 0;
    			if(targetBounds.min.y < bottom){
    				shiftY = targetBounds.min.y - bottom;
    			}else if (targetBounds.max.y > top){
    				shiftY = targetBounds.max.y - top;
    			}
    			top += shiftY;
    			bottom += shiftY;
    			centre = new Vector2((left + right) / 2, (top + bottom) / 2);
    			velocity = new Vector2(shiftX, shiftY);
    		}
    	}
    }
    Descargar código completo

    Gestionar las animaciones del personaje

    Vamos a sustituir el cubo por las correspondientes animaciones. Para ello, definiremos el método ApplyAnimations dentro de una nueva clase Anims que se encargará de activar los correspondientes parámetros del animator.

    Llamaremos al método ApplyAnimations al final del método Update de la clase player.

    RaycastController.cspublic virtual void Awake(){
    	collider = GetComponent<BoxCollider2D>();
    }
    
    public virtual void Start(){
    	collider = GetComponent<BoxCollider2D>();
    	CalculateRaySpacing();
    }

    Soporte para Oculus Quest

    Debemos usar como mínimo la versión 2018.4 para arriba

    Configuración inicial

    Instalar el oculus plugin

    1. Unity menu -> Window -> Asset Store
    2. Escribimos "Oculus integration" en el buscador.
    3. Instalamos el primer resultado (un plugin gratuito hecho por Oculus).

    Instalar los driver de oculus

    1. Descargamos los drivers de Oculus.
    2. Descomprimimos el zip -> pulsamos con el botón derecho sobre el fichero android_winusb -> instalar como administrador.

    Desbloquear el developer mode en las quest

    1. Antes de nada, tendremos que crear una organización en la página de Oculus.
    2. En la oculus App del móvil -> settings
    3. Pulsamos en el icono de mis oculus quest para hacer el pairing.
    4. En el menú contextual pulsamos sobre -> other settings -> Developer mode -> lo activamos

    Build Settings

    1. File -> Build Settings
      1. Platform: Android -> Switch Platform
      2. Texture Compression: ASTC
      3. Player Settings
        1. XR Settings -> Virtual Reallity Supported -> Marcamos la check -> Virtual reality SDKs -> le damos a la cruceta para añadir oculus
        2. Other Settings -> Minimum API Level ->Seleccionamos la mínima versión del SDK (23 es la recomendada):

    Ejecutar en las oculus

    1. Para que el ordenador detecte las oculus...
      • Nescesitamos conectarlas con un cable que no sólo sirva para cargar, si que también permita la transferencia de datos.
      • Tendremos que arrancar la aplicción de Unity. Una vez abierta, es posible que tengamos que ejecutar File -> build settings y hacer Switch Platform a Android.
      • Ejecutamos el siguiente comando (suponiendo que tenemos adb instalado):
        CameraFollow.cspublic float verticalOffset;
        
        void LateUpdate(){
        	...
        	Vector2 focusPosition = focusArea.centre + Vector2.up;
        	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
        }

    Añadir personaje

    1. Eliminamos la MainCamera (o si no, cuando añadamos el siguiente objeto, tendremos dos cámaras).
    2. Añadimos a la escena el ovrPlayerController.
    3. Añadimos un suelo para que el player no se caiga infinitamente
    4. Probamos la app -> Podemos girar la cabeza y movernos con el joistick
    5. Mejoramos el rastreo de la cabeza. Al objeto OVRPlayerController le añadimos el componente CharacterCameraConstraint. A este componente le asociamos la propiedad CameraRig con el valor de OVRCameraRig del OVRPlayerController. También le marcamos la check autoheight.

    Añadir mandos

    1. Añadimos los siguientes prefabs al objeto TrackingSpace del OVRPlayerController
      • LocalAvatar
      • AvatarGrabberLeft
      • AvatarGrabberRight
    2. Para que las manos funcionen es necesario introducir la APP Id (que podemos encontrar en https://dashboard.oculus.com/). Si no hubiesemos creado una organización y una app tendríamos que crearlas. Introduciremos la APP ID en:
      • Oculus > Avatars > Edit Settings
      • Oculus > Platoform > Edit Settings

    Ubicar un panel con código

    Para ubicar un panel no usaremos Transform sino RectTransform.

    En lugar de deginir dónde estará la capa, definiremos su esquina superior derecha y su esquina inferior izquierda:

    CameraFollow.csfloat currentLookAheadX;
    float targetLookAheadX;
    float lookAheadDstX;
    float lookAheadDirX;
    float lookSmothTimeX;
    float smoothLookVelocityX;
    
    void LateUpdate(){
    	focusArea.Update(target.collider.bounds);
    	Vector2 focusPosition = focusArea.centre + Vector2.up;
    
    	if(focusArea.velocity.x != 0){
    		lookAheadDirX = Mathf.Sign(focusArea.velocity.x);
    	}
    	targetLookAheadX = lookAheadDirX * lookAheadDstX;
    	currentLookAheadX = Mathf.SmoothDamp(currentLookAheadX, targetLookAheadX, ref smoothLookVelocityX, lookSmothTimeX);
    	focusPosition += Vector2.right * currentLookAheadX;
    
    	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
    }

    Máscara en Unity 3D

    Sin código

    Añadimos a un panel un componente de máscara. Cuando el componente de máscara esté activo, sólo se verá la parte de sus hijos que estén ubicados dentro.

    Con animación

    La siguiente corrutina hace que la máscara se despliegue

    CameraFollow.cspublic float lookSmoothTimeX;
    bool lookAheadStopped;
    float smoothVelocityY;
    public float verticalSmoothTime;
    
    void LateUpdate(){
    	focusArea.Update (target.collider.bounds);
    	Vector2 focusPosition = focusArea.centre + Vector2.up * verticalOffset;
    	if(focusArea.velocity.x != 0){ 
    		lookAheadDirX = Mathf.Sign(focusArea.velocity.x);
    		if(Mathf.Sign(target.playerInput.x) == Mathf.Sign(focusArea.velocity.x) && target.playerInput.x !=0){
    			lookAheadStopped = false;
    			targetLookAheadX = lookAheadDirX * lookAheadDstX;
    		}else{
    			if (!lookAheadStopped){
    				lookAheadStopped = true;
    				targetLookAheadX = currentLookAheadX + (lookAheadDirX * lookAheadDstX - currentLookAheadX) / 4f;
    			}
    		}
    	}
    	currentLookAheadX = Mathf.SmoothDamp(currentLookAheadX, targetLookAheadX, ref smoothLookVelocityX, lookSmoothTimeX);
    
    	focusPosition.y = Mathf.SmoothDamp(transform.position.y, focusPosition.y, ref smoothVelocityY, verticalSmoothTime);   
    	focusPosition += Vector2.right * currentLookAheadX;
    }

    Ejercicio

    Escribir una corrutina para que la máscara se repliegue.

    Swipe panel

    Sin código

    Debo crear la siguiente jerarquía de paneles:

    • Viewport (Con el componente Scroll Rect asociado, tendrá el ancho de la pantalla)
      • Content (tendrá el ancho de todos los paneles que contiene)
        • panel1
        • panel2

    Con ajuste repentino al inicio

    void LateUpdate(){
    	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
    }

    Con ajuste repentino al moverme

    public class PlayerAnims : MonoBehaviour{
    	Player player;
    	SpriteRenderer spr;
    	bool flipped = false;
    	void Start(){
    		player = GetComponent();
    		spr = GetComponent();
    	}
    
    	void Update(){
    		FlipSprite();
    	}
    
    	void FlipSprite(){
    		if(flipped && player.velocity.x > 0) {
    			flipped = false;
    			spr.flipX = false;
    		}else if(!flipped && player.velocity.x < 0){
    			flipped = true;
    			spr.flipX = true;
    		}
    	}
    }

    Con Ajuste animado

    Para que al soltar el panel este no tiemble, a la propiedad Movement Type del componente Scroll Rect el valor Clamped, en lugar de Elastic.

    adb install -r \ruta-apk\prueba.apk

    Memory (juego de las parejas)

    Descargar juego de las parejas

    Ubicar objetos en el canvas

    Creo la siguiente jerarquía de objetos dentro del camvas:

    • Panel (con el componente vertical layout group)
      • panel1 (con el componente horizontal layout group)
        • carta1
        • carta2
        • carta3
        • carta4
        • carta5
      • panel2 (con el componente horizontal layout group)
        • carta1
        • carta2
        • carta3
        • carta4
        • carta5

    Voltear cartas

    //Defino las coordenadas de las esquina superior derecha de la capa
    panel.GetComponent<RectTransform> ().offsetMax = new Vector2 (-70,-70);
    // y de la esquina inferior izquierda
    panel.GetComponent<RectTransform> ().offsetMin = new Vector2 (70,70);
    public RectTransform panel;
    
    	IEnumerator Desplegar(){
    	float t = 0;
    
    	while (t <= 1) {
    		t += 0.1f;
    		panel.offsetMin = new Vector2 (panel.offsetMin.x, Mathf.Lerp(306, 0, t));
    		yield return new WaitForSeconds (0.05f);
    	}
    	yield return null;
    }

    La carta llama al GameController

    Destapo dos cartas

    public ScrollRect scrollRect;
    
    void Start () {
    	scrollRect.horizontalNormalizedPosition = 1;	
    }
    void Update(){
    	if(Input.GetMouseButtonUp(0)){
    		snap ();
    	}
    }
    
    void snap(){
    	float valorActual = scrollRect.horizontalNormalizedPosition;
    	valorActual = Mathf.Round (valorActual);
    	scrollRect.horizontalNormalizedPosition = valorActual;
    }

    Añadir la detección de cuando se ha ganado la partida

    Guardar y recuperar información almacenada

    void Update(){
    	if(Input.GetMouseButtonDown(0)){
    		//Si vuelvo a pulsar, cancelo la corrutina anterior para que no se me amontonen corrutinas/*
    		StopCoroutine ("Transicion");
    	} else if(Input.GetMouseButtonUp(0)){
    		snap ();
    	}
    }
    
    void snap(){
    	float valorFinal = scrollRect.horizontalNormalizedPosition;
    	Debug.Log ("Valor final: " + valorFinal);
    	int cantidadPosiciones = 3;
    
    	valorFinal = Mathf.Round (valorFinal * (cantidadPosiciones-1)) / (cantidadPosiciones-1);
    	StartCoroutine ("Transicion", valorFinal);
    }
    
    IEnumerator Transicion(float valorFinal){
    	float valorInicial = scrollRect.horizontalNormalizedPosition;
    
    	float incremento = (valorFinal > valorInicial) ? 0.01f : -0.01f;
    
    	float direccion = valorInicial - valorFinal;
    
    	while (true){
    		valorInicial += incremento;
    		if (Mathf.Sign(direccion) != Mathf.Sign(valorInicial - valorFinal)) break;
    		scrollRect.horizontalNormalizedPosition = valorInicial;
    		yield return new WaitForSeconds(0.05f);
    	}
    //Los últimos pixeles, los ajustamos a la fuerza
    scrollRect.horizontalNormalizedPosition = valorFinal;
    }
    GameController.cspublic class GameController : MonoBehaviour {
    	public Sprite[] cardFace;
    	public Sprite cardBack;
    	public GameObject[]cards;
    
    	void Start(){
    	for (int id = 0; id < 2; id++) {
    	for (int i = 0; i <cardFace.Length; i++) {
    	bool test = false;
    	int choice = 0;
    
    	while (!test) {
    		choice = Random.Range (0, cards.Length);
    		//Voy buscando hasta encontrar una carta no inicializada
    		//Si todas las cartas fueron ya inicializadas, obtendré un bucle infinito
    		test = !(cards [choice].GetComponent<Card> ().initialized);
    	}
    	cards [choice].GetComponent<Card> ().cardFace = cardFace[i];
    	cards [choice].GetComponent<Card> ().initialized = true;
    	}
    }
    Descargar recurso

    Publicación

    File -> Build Settings

    Pulsando en el botón Player Settings podré definir algunas propiedades de la exportación.

    Player settings

    Publicación para Desktop

    Display Resolution Dialog: Disabled. Mejor ponerlo en disabled, no vaya a ser que el usuario seleccione una resolución que nosotros no habíamos contemplado.

    Publicación para web

    Debemos asegurarnos de que la proporción de la pantalla de publicación coincide con el tamaño que le hemos asignado al juego.

    Unity publicación web

    Esta proporción también se puede cambiar fuera de unity, modificando el código html del fichero creado:

    Card.csusing UnityEngine;
    using UnityEngine.UI;
    
    public class Card : MonoBehaviour{
    	public bool initialized = false;
    
    	public Image cardBack;
    	public Sprite cardFace;
    	GameController gc;
    	void Start(){
    		gc = GameObject.Find("GameController").GetComponent<GameController>();
    		cardBack = GetComponent<Image>();
    		cardBack.sprite = gc.cardBack;
    		GetComponent<Button>().onClick.AddListener(flipCard);
    	}
    
    	public void flipCard(){
    		GetComponent<Image>().sprite = cardFace;
    	}   
    }

    Ejecutar la aplicación directamente en nuestro dispositivo Android

    Habrá que activar el modo depuración USB. Para ello, habrá que activar las opciones de desarrollo (en información del teléfono, haremos click sietes veces sobre el número de compilación).

    Finalmente le daremos a build and run teniendo el teléfono móvil conectado por USB.

    Errores al compilar

    • GameController.csprivate Card primeraCarta;
      private Card segundaCarta;
      
      public void evaluarCartas(Card4 carta){
      	if (primeraCarta == null) {
      		carta.GetComponent<Image> ().sprite = carta.cardFace;
      		primeraCarta = carta;
      	} else if (segundaCarta == null) {
      		carta.GetComponent<Image> ().sprite = carta.cardFace;	
      		segundaCarta = carta;
      		if (primeraCarta.GetComponent<Image> ().sprite == segundaCarta.GetComponent<Image> ().sprite) {
      			Debug.Log ("exito");
      			primeraCarta = null;
      			segundaCarta = null;
      		} else {
      			StartCoroutine (dejaCartasComoAlPrincipio());	
      		}
      	}
      }
      
      IEnumerator dejaCartasComoAlPrincipio(){
      	yield return new WaitForSeconds (1f);
      	primeraCarta.GetComponent<Image> ().sprite = cardBack;
      	segundaCarta.GetComponent<Image> ().sprite = cardBack;
      	primeraCarta = null;
      	segundaCarta = null;
      }
      Este error me dió en linux al compilar para Android. Se soluciona ejecutando le siguiente comando en la terminal:
      Card.cs private void flipCard(){
      	// this.GetComponent<Image>().sprite = _cardFace;
      	gc.evaluarCartas(this);
      }
    • Encontrar el SDK de Android. Lo instalaremos junto al Android Studio descargándolo de este link. En el caso de linux, se instala en la carpera /root/Android/Sdk, a la que tendremos que dar permisos 777 para poder seleccionar.

    Otros Errores

    1. Si al abrir un proyecto de Unity nos dá el siguiente error: 'it looks like another unity instance is running with this project open. Multiple Unity instances cant open the same project.' Podemos intentar solucionar el problema de dos formas:
      1. Borrar el fichero Temp/UnityLockFile dentro de la carpeta de nuestro proyecto.
      2. Dubplicar el proyecto utilizando otro nombre.
    2. Si Unity se queda colgado al tratar de abrir un proyecto, podemos tratar a borrar la carpeta Library de dicho proyecto.

    Ejercicio: First Person Shooter

    Crear la siguiente escena con su correspondiente FPSController

    ejercicio unity 3d

    Importamos los assets necesarios: Window -> Asset store -> Standard Assets -> importamos:

    • Characters -> FirstPersonCharacter. Este asset tiene las siguientes dependencias:
      • CrossPlatformInput
      • Utility

    Además, podemos descargar una casita:

    • Prototyping -> Models -> HousePrototype16x16x24

    Todos los elementos que se muestran en escena son cubos deformados, menos las casas que las hemos insertado desde los assets importados.

snapping: lo haremos si mantenemos pulsado v desde antes de la selección de uno de los vértices de un objeto y su posterior desplazamiento.

Para controlar la distancia a partir de la cual las sombras se proyectan: edit -> proyect settings -> Quality -> Shadows -> Shadow Distance

FPSController

Este es un objeto complejo que tiene asociados los controles del ratón y el teclado, posibilidad de correr, saltar, etc. tal como ocurre en un First Person Shooter.

Al caminar, el FPSController emite los sonidos de los pasos que se dan, se pueden deshabilitar poniendo a 0 la propiedad Footstep Sounds-Size del FPSController en el panel inspector.

Para deshabilitar el movimiento de la cabeza al andar, desmarco la check de la propiedad Use Head Bob.

Para habilitar la visualización del cursor durante el juego, desmarco la check Mouse Look - Lock Cursor


Al ejecutar, obtenemos este error: There are 2 audio listeners in the scene. Please ensure there is always exactly one audio listener in the scene.. Se debe a que hay dos cámaras, la del FPSController, y la Main Camera. Deshabilitamos la Main Camera pulsando sobre la check que está al lado de su nombre.

Importar personaje 3D a la escena

  1. Assets Store -> Descargamos Junk Chan -> Lo importamos todo.
  2. Vamos a la carpeta Models -> arrastramos y soltamos el modelo de JunkChan.
  3. Creamos una entidad Animator.
  4. Al desplegar el modelo pulsando en su flechita salen todas sus animaciones. Arrastramos al animator las animaciones idle, walk, Sword_Slash.

Multijugador con Photon

Vamos a utilizar los servidores de Photon para hacer un juego multijugador. Estos servidores tienen instalada una aplicación llamada PhotonServer. Nosotros también podríamos instalar PhotonServer en nuestro propio servidor, pero utilizando los servidores de Photon nos ahorramos este paso. Los servidores de Photon reciben el nombre de Photon Cloud

PUN (Photon Unity NetWorking) es un framework que tiene scripts que nos facilitan programar videojuegos multiplayer. PUN es:

Conexión y Join una Room

  1. Registramos una cuenta en https://www.photonengine.com/
  2. Tras loguearnos creamos una nueva aplicación cuyo Photon Type será Photon PUN.
  3. Copiamos el App ID de la aplicación.
  4. Vamos a Unity 3D e instalamos Photon Networking 2 Free. Al termino de la instalación nos pide un código. Pegamos el código de antes.
  5. Creamos un Empty al que vinculamos el siguiente código:
    1. Cuando pulsamos el botón de Connect, llamamos a la función Connect()
    2. Cuando nos hemos conectado, se ejecuta automáticamente el método OnConnectedToMaster(). Este método habilitará el botón Join Random
    3. Cuando pulsamos el botón de Join Random, llamamos a la función JoinRandom()
    4. Cuando nos hemos unido a una room, se ejecuta automáticamente el método OnJoinedRoom()(). Dentro del FixedUpdate, evaluamos constantemente si nos hemos unido a una Room.Este método habilitará el botón Join Random
    using System.Collections;
    using System.Collections.Generic;
    using Photon.Pun;
    using Photon.Realtime;
    using UnityEngine;
    using UnityEngine.UI;
    namespace Photon.Pun.Demo.PunBasics{
    public class AutoLobby : MonoBehaviourPunCallbacks{
    public Text Log;
    public Text PlayerCount;
    public int playersCount;
    public byte maxPlayersPerRoom = 4;
    public byte minPlayersPerRoom = 2;
    private string roomName = "amor";
    public Button buttonJoinRoom;
    public Button buttonLoadArena;
    public Text nombre;
    void Start(){
    // La IP del servidor al que se conectará el player será almacenada en las PlayerPrefs.
    // Debemos resetear las PlayerPrefs para evitar ciertos problemas de conexión
    PlayerPrefs.DeleteAll();
    ConnectToPhoton();
    }
    void Awake(){
    // La siguiente línea nos permite sincronizar la escena para todos los players de la room
    PhotonNetwork.AutomaticallySyncScene = true;
    }
    void ConnectToPhoton(){
    Log.text = "Connecting...";
    PhotonNetwork.GameVersion = ""+1; //1
    if (PhotonNetwork.ConnectUsingSettings()){
    Log.text += "\n Connected to server";
    }else{
    Log.text += "\n Falling Connecting to Server";
    }
    }
    // Photon Methods
    public override void OnConnected(){
    base.OnConnected();
    Log.text += "\nConnected to Photon!";
    buttonJoinRoom.interactable = true;
    buttonLoadArena.interactable = false;
    }
    public void JoinRoom(){
    Log.text += "\nJoinRoom() method";
    if (PhotonNetwork.IsConnected){
    PhotonNetwork.LocalPlayer.NickName = nombre.GetComponent<Text>().text; //1
    Log.text += "\nPhotonNetwork.IsConnected! | Trying to Create/Join Room ";
    RoomOptions roomOptions = new RoomOptions(); //2
    TypedLobby typedLobby = new TypedLobby(roomName, LobbyType.Default); //3
    PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, typedLobby); //4
    Log.text +="\nFin JoinRoom(): " + PhotonNetwork.LocalPlayer.NickName;
    }
    }
    public override void OnJoinedRoom(){
    if (PhotonNetwork.IsMasterClient){
    buttonJoinRoom.interactable = false;
    buttonLoadArena.interactable = true;
    Log.text += "\nYour are Lobby Leader";
    }else{
    Log.text += "\nConnected to Lobby";
    }
    }
    public void LoadArena(){
    if (PhotonNetwork.CurrentRoom.PlayerCount > 1){
    PhotonNetwork.LoadLevel("juego");
    }else{
    Log.text += "\nMinimum 2 Players required to Load Arena!";
    }
    }
    void Update(){
    if (PhotonNetwork.InRoom){
    playersCount = PhotonNetwork.CurrentRoom.PlayerCount;
    PlayerCount.text = playersCount + "/" + maxPlayersPerRoom;
    }
    }
    public override void OnJoinRandomFailed(short returnCode, string message){
    Log.text += "\nNo Rooms to Join, creating one...";
    if (PhotonNetwork.CreateRoom(null, new Photon.Realtime.RoomOptions() { MaxPlayers = maxPlayersPerRoom })){
    Log.text += "\n Room Created";
    }else{
    Log.text += "\nFail Creating Room";
    }
    }
    }
    }
multijugador con photon

Cargamos la escena de juego

  1. AutoLobby.cs... private void LoadMap(){ PhotonNetwork.LoadLevel("juego"); }
  2. GameController.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using Photon.Pun; using UnityEngine.UI; namespace Photon.Pun.Demo.PunBasics{ public class GameController : MonoBehaviourPunCallbacks{ private GameObject player1; private GameObject player2; public Transform player1Position; public Transform player2Position; void Start(){ if (PlayerManager.LocalPlayerInstance == null){ if (PhotonNetwork.IsMasterClient){ player1 = PhotonNetwork.Instantiate("PlayerMonteserin", player1Position.transform.position, player1Position.transform.rotation, 0); }else{ player2 = PhotonNetwork.Instantiate("PlayerMonteserin", player2Position.transform.position, player2Position.transform.rotation, 0); } } } } }
  3. Creamos un Prefab de nuestro player.

    Importaremos Joystick Pack de la Asset Store de Unity. En la ruta Joystick Pack/Prefabs usaremos el Prefab Floating Joystick que debemos añadir a un canvas.

    joystick pack

    Utilizaremos el siguiente código para gestionar su movimiento. El prefab del Player tendrá un cubo de tal forma que cuando lo pulsemos subirá para arriba:

    PlayerController.csusing System.Collections; using System.Collections.Generic; using Photon.Pun; using UnityEngine; public class PlayerController : Photon.Pun.MonoBehaviourPun{ public bool isMovingUp, isMovingDown = false; Rigidbody rb; private Vector2 movement; public Joystick joystick; PhotonView photonView; void Start(){ photonView = gameObject.GetComponent<PhotonView>(); rb = GetComponent<Rigidbody>(); joystick = GameObject.Find("Canvas").transform.Find("Floating Joystick").GetComponent<FloatingJoystick>(); } void FixedUpdate(){ if (photonView.IsMine){ this.movement = this.rb.velocity; if (joystick.Horizontal > 0)this.movement.x = 2; else if (joystick.Horizontal < 0) this.movement.x = -2; else this.movement.x = 0; if (joystick.Vertical > 0) this.movement.y = 2; else if (joystick.Vertical < 0) this.movement.y = -2; else this.movement.y = 0; rb.velocity = movement; } } }
  4. Añadimos al GameObject del Prefab los siguientes scripts:
    • PhotonView ubicado en /Assets/Photon/PhotonUnityNetworking/Code/
    • PhotonTransformView ubicado en /Assets/Photon/PhotonUnityNetworking/Code/
  5. Arrastramos el componente PhotonTransformView al campo Observed ComponentsTransformView.
  6. Debemos asegurarnos de que el TransformView tiene la opción Observe option con el valor Unreliable On Change. Photon unreliable on change
  7. En este momento, si compilamos el juego y lo ejecutamos dos veces, veremos que al mover el personaje en una de las dos instancias de juego, este se mueve en la otra instancia de juego. Es mejor que probemos el juego en dispositivos independientes. Arrancando dos instancias del juego en el mismo dispositivo, la sincronización era bastante mala.
photon2 Descargar recurso

Ejercicio: composición 2D

Ubicar elementos 2D en capas

  1. Importar los assets del enlace.
    assets Guardar assets1.png y assets2.png en una carpeta llamada sprites.
  2. Convertir el sprite mode del Assets1 a múltple y salvar los sprites que contiene, cada uno con su propio nombre. Para hacer los recortes usaremos el modo automático. Guardar las ruedas en un único sprite.
  3. Hacer lo mismo con con Assets2
  4. Añadimos un sprite a escena y seleccionándolo, desde su componente sprite renderer, podremos crear tres sorting layers: BG, INTERMEDIO y FRONT.
  5. Añado los elementos a escena, de atrás alante, en este orden de superposición:
    1. BG: nubes, nube1, nube2,ciudad (la foto con un pedazo de noria), ciudad (la foto con los edificios).
    2. INTERMEDIO: torre1 (pegada ala izquierda) y torre2 (pegada a la derecha, es una copia de torre1 rotada 180º en el eje x, para que sea simétrica). Autobús (agrupado a las ruedas), coche (agrupado a las ruedas).
    3. FRONT: paseo.
paseo para unity3d

Añadir sonido

  1. En el panel de jerarquía creamos dos emptys: sonidos y música. Les añadimos el componente Audio Source y lo vinculamos con el sonido correspondiente
  2. Para que funcionen los sonidos, la main camera debe tener un Audio Listener; podemos borrar el GUI layer, y el flare layer ya que no los necesitamos.

Recuperar un array de objetos del mismo tipo

public class GetArrayObjetos : MonoBehaviour {
public MiCubo [] misCubos;
void Start () {
misCubos = Object.FindObjectsOfType (typeof(MiCubo))as MiCubo[];
Debug.Log (misCubos.Length);
}
}
Descargar ejemplo

Jerarquías: recuperar hijos

public class Jerarquias : MonoBehaviour {
public Transform miTransform;
void Start () {
miTransform = GetComponent<Transform> ();
for (int i = 0; i < miTransform.childCount; i++) {
Transform hijoTransform = miTransform.GetChild (i);
Debug.Log (hijoTransform.gameObject.name);
}
}
}
Descargar ejemplo

Detectar cuando suelto algo encima de otro objeto

public class Slot : MonoBehaviour , IDropHandler{
public void OnDrop(PointerEventData eventData){
Debug.Log ("suelta");
//eventData.pointerDrag es el objeto que hemos soltado sobre este objeto
Debug.Log (eventData.pointerDrag.name);
}
}

Este evento detecta la colisión entre el ratón y el objeto B sobre el que he soltado una pieza A. Sin embargo, si la pieza A está entre el ratón y la pieza B, dicha colisión no será detectada. Para evitarlo, puedo añadir al botón A un componente de tipo <<Canvas Group> desactivarlo cuando lo comience a arrastrar.

public class ObjetoA : MonoBehaviour, IDragHandler , IBeginDragHandler, IEndDragHandler, IPointerDownHandler{
public void OnBeginDrag(PointerEventData eventData){
GetComponent<CanvasGroup> ().blocksRaycasts = false;
}
public void OnEndDrag(PointerEventData eventData){
GetComponent<CanvasGroup> ().blocksRaycasts = true;
}
}
icono de mandar un mailPreguntame lo que quieras!
Pablo Monteserín
contacta conmigoPablo Monteserín

El servicio de resolución de dudas técnicas es sólo para los usuarios premium. Si tienes cualquier otra duda, usa el formulario de contacto. ¡Gracias!