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. Máscara
  31. Publicación
  32. Ejercicio: First Person Shooter
  33. Multijugador con Photon

🕹 Unity 3D
Diseño Web

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

  • Nubecita: Sólo para cuentas pro. Nos permite trabajar en la nube.
  • Último desplegable (layout): Trabajaremos en el valor default.
  • Penúltimo desplegable (layers): Esto controla que capas son visibles o invsibles. Si nos ponemos a mariposear y desactivamos el ojo de la capa default, no veremos nada.
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

De un GameObjet

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

De un Sprite Renderer

Botón derecho sobre el panel de proyecto -> new -> sprite -> Asociamos el sprite al spriterenderer -> una vez asociado le podemos cambiar el 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 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 ();
}
}
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");
	}
}

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:

  • Source: color
  • Ambient Color: blanco puro

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);
	}
}

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);
}

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:

  • Screen Space - Overlay: hace que el canvas se adapte a todo el tamaño de la pantalla.
  • Screen Space - Camera: hace que el canvas se adapte a una cámara concreta. De esta forma, podremos usar varios canvas
  • World Space. Me permite integrar el canvas en el entorno de la escena (el mundo que estamos creando).

Canvas Scaller

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

  • Constant Pixel Size: los componentes tendrán un tamaño fijo.
  • Match Width or Height: los componentes se deformarán con la pantalla, en función de su ancho o alto.

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.

  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.
  3. Ejercicio - Hacer un piano

    1. Creamos un nuevo proyecto 2D.
    2. Creamos una carpeta "sounds", e importamos en ella los 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.
  4. Vincular un Input de texto a un GameObject y recuperar su valor.

    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: Screen 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

    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

    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.
    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.

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.

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

  • Sorting Layer: nos permite vincular un objeto a una capa. Los objetos vinculados a las capas que están por encima, se verán también por encima.
  • Order in Layer: Nos permite definir la ubicación del objeto dentro de una capa. Los objetos con números mayores se verán por encima dentro de dicha capaNos permite determinar que sprite se mostrará por encima.

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.

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.

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

marcador con unity

TextMeshPro

Esta entidad nos permite introducir texto en pantalla.

3D Object -> TextMesh Pro - Text

En el siguiente pantallazo...

Textmeshpro
  • El cuadrado verde es el boxcollider del TextMeshPro.
  • El cuadrado con vértices azules delimita los límites del area de margen que puedes cambiar en el propio componente en Extra Settings -> Margins
  • El cuadrado amarillo es el area del texto.

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

Cargar una tipografía

Menú window -> TextMeshPro -> Font Asset Creator -> Arrastramos y soltamos la fuente en Source Font File -> Generate Font Atlas -> save -> El fichero creado es el que puedo arrastar y soltar en el inspector para poder usarlo.

Cuando dentro del inspector, editmos el material de la fuente, realmente estaremos editando el material de todos los casos en los que se esté usando esta fuente.

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 <TextMeshPro> ().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

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

  • Dynamic: El cuerpo es afectado por las fuerzas que interactúan con él.
  • Kinematic: El cuerpo no es afectado por las fuerzas que interactúan con él.
  • Static: Ideal para plataformas estáticas.

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
  • Entry es el punto de partida de la animación.
  • En muchos casos, el estado Entry va vinculado a un estado creado por nosotros llamado idle, que corresponde a una animación del objeto en reposo.
  • Obligatoriamente, el estado Entry debe estar unido con algún otro estado.
  • Cuando una animación llega al exit, regresa al 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:

    Como imagen del canvas (forma recomendada porque podremos controlar cómodamente el grado de repetición de la imágen de fondo)

    1. Botón derecho sobre el panel de jerarquía -> UI -> Image.
    2. Arrastro las esquinas para que ocupen todo el canvas.
    3. En el inspector:
      • Seleccionamos el canvas que envuelve a la imagen:
        • Render mode: Screen Space - Camera
        • Plane distance: ponemos un número suficientemente alto como para que el resto de objetos se situen por encima.
        • Order in layer: -1
        • Canvas Scaler ⟶ UI Scale Mode ⟶ Scale With Screen Size
      • Seleccionamos la imagen recién añadida: Image type tiled.
        • Modificamos el rect transform de la imagen para anclarlo a las cuatro esquinas del canvas.
    Redimensionar canvas en Unity 3D

    Como Objeto de imagen: Arrastro la imagen. Se inserta como gameobject con un componente SpriteRenderer. Accedemos a su inspector:

      • Draw Mode: Tiled.
      • Cambio los parámetros del Size (la escala no).

Instanciar

Instanciar un GameObject y añadirle propiedades dinámicamente

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

¿Qué es 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.

Dos formas de instanciar un prefab

  • Crear una propiedad pública de tipo GameObject en un script vinculado a un objeto y arrastrar el prefab desde el panel de proyecto al panel de jerarquí en el que se encuentra dicha propiedad. GameObject nuevoMisil = Instantiate(prefabMisil, new Vector2(2,2), Quaternion.identity);
  • Almacenar el prefab en la carpeta Resources y utilizar su nombre para instanciarlo:
    GameObject word1 = Instantiate(Resources.Load("RandomWord"), new Vector2(0.1f, 0.1f), Quaternion.identity) as GameObject;

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.

  • Si sólo hay un hijo, este estará en sus coordenadas 0,0,0.
  • Si hay varios hijos, deberían orbitar alrededor del punto 0,0,0 del padre.
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.
  • Cuando la check está marcada, los objetos que colisionan se superponen durante la colisión, pasando uno por encima de otro. El código que usamos para gestionar estas colisiones es similar a este:
    void OnTriggerEnter2D(Collider2D col){
    }
  • Cuando la check no está marcada, los objetos que colisionan impactan entre sí.
    void OnCollisionEnter2D(Collision2D col){
    }

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.

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.

Descargar ejemplo
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);
}
}

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

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

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

  • Añadir un marcador que se incrementa cuando la pelota sale por uno de los lados de la pantalla.
  • Meter la escena de perder el juego.

Carreras

Ponemos la imágen de fondo

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:

  • Cambiamos la propiedad Gravity Scale a 0.
  • Definimos la propiedad Constraints -> Freeze Rotation: 0.
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:

  • Aumentar mucho el Angular Drag
  • Marcar la check de Freeze Rotation.

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:

  • Duplicamos el Game Object del jugador que ya está insertado para obtener los Game Object de los otros tres jugadores. Debemos definir una nueva animation y un nuevo animator por cada jugador.
  • Al hacer la animación tener en cuenta que las animaciones cíclicas, deben empezar y terminar por el mismo frame.
  • Para poder añadir una animación a un objeto, es necesario que el objeto esté seleccionado en el panel de jerarquía.
  • Finalmente, el código asociado a cada player, quedaría así:
    public class Player : MonoBehaviour{
        float speed = -2;
        private Rigidbody2D rb;
    
        public KeyCode keycodeLeft;
        public KeyCode keycodeRight;
        private Animator animator;
    
    	void Start(){
            rb = GetComponent<Rigidbody2D>();
            animator = GetComponent<Animator>();
        }
    
        void Update() {
            if (Input.GetKey(keycodeLeft)){
                Debug.Log("gira");
                transform.Rotate(0,0,90 * Time.deltaTime);
            }
            if (Input.GetKey(keycodeRight)){
                transform.Rotate(0, 0, -90 * Time.deltaTime);
            }
            rb.velocity = transform.right * speed;
        }
    }

Colocar marcador

Para cada jugador añadimos dos objetos de tipo TextMeshPro. 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

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");
	}
}

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.

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.

  • idle: será la animación por defecto. Es una animación sin fotogramas clave.
  • horizontal: es una animación con dos fotogramas clave. El cubo se moverá horizontalmente.
  • vertical: es una animación con dos fotogramas clave. El cubo de moverá verticalmente.
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

Ponemos al prota en pantalla

  • Debe tener la animación de respirar.
  • Debe caer por gravedad
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

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);
	}
}

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 de seguir el trazado

Hacer un juego en el que el usuario debe arrastrar una bolita por el camino fijado.

Juego arrastrar y soltar

Juego de seguir trazado

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.

Haremos un único area de colisión para cada número y cuando el usuario salga de él, mostraremos un popup.

El objeto TextMeshPro 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:

objetoHijo.transform.parent.parent

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. 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();
    	}
    }
  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.

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.

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;
	}
}
Controller2D.cspublic void Move(Vector3 velocity){
	transform.Translate(velocity);
}

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

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);
		}
	}
}
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;
	}
}

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
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);
	...

Detección de colisión

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;
		}
	}
}

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.

No te olvides de vincular la nueva clase PlayerInput.cs que vamos a crear al Player)

PlayerInput.cs[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);
	}
}
Player.csfloat moveSpeed = 6;
Vector2 directionalInput;

void CalculateVelocity(){
	velocity.x = directionalInput.x * moveSpeed;
	...
}

public void SetDirectionalInput(Vector2 input){
	directionalInput = input;
}

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.

Controller2D.csvoid VerticalCollisions(ref Vector3 velocity){
	...
	rayOrigin += Vector2.right * verticalRaySpacing * i;
	rayOrigin += Vector2.right * (verticalRaySpacing * i + velocity.x);
	...
}

Colisión horizontal

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);
}
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;
		}
	}
}

Gestionar colisiones sólo cuando el player se mueva

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);
}

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.

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;
}
Player.csvoid Update(){
	CalculateVelocity();
	controller.Move(velocity * Time.deltaTime);

	if (controller.collisions.above || controller.collisions.below){
		velocity.y = 0;
	}
}

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.

Controller2D.cspublic void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	collisions.Reset();
	...
}

public struct CollisionInfo{
	...

	public void Reset(){
		above = below = false;
		left = right = false;
	}
}

Saltar

Player.csfloat jumpVelocity = 8;
public void OnJumpInputDown(){
	if (controller.collisions.below){
	velocity.y = jumpVelocity;
	}
}
PlayerInput.csUpdate(){
	...
	if (Input.GetKeyDown(KeyCode.Space)){
		player.OnJumpInputDown();
	}
}

Transformar jumpHeight y timeToJumpApex en gravity y velocity

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

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;
}

Deceleración progresiva en los cambios de dirección

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;
}

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.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); 
}

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.

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;
	}
}

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.)

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;
	}
}

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
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;
	}
	...
}

Corrección del tembleque al colisionar por arriba

problema al colisionar contra un techo
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;
		}
	}
}

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 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;
	}
}

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
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;
	...
}

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.
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);
    }
}

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.
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));
				}
			}
		}
	}
}

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.

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));
				}
			}
		}
	}
}

Corregir errores del movimiento horizontal y hacia abajo de la plataforma

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));
				}
			}
		}
	}
}

Gestionar la colisión de la plataforma con paredes

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

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;
	}
}

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.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);
		}
	}
}

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.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);
		}
	}

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

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

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.

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

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.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);
		}
	}
}

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.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;
}

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.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);
			}
		}
	}

waitTime

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

PlatformController.cspublic float waitTime;
float nextMoveTime;

Vector3 CalculatePlatformMovement(){
	if(Time.time < nextMoveTime){
		return Vector3.zero;
	}
	...
	if(percentBetweenWaypoints >= 1){
		...
		nextMoveTime = Time.time + waitTime;
	}
}

Aceleración del movimiento

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);

Wall jumping

Caer derrapando contra una pared vertical

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;
		}
	}
}

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

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;
	}
	...
}

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
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;
	...
}

Salto a varias alturas

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;
	}
}
PlayerInput.csvoid Update(){
	...
	if (Input.GetKeyUp (KeyCode.Space)) {
		player.OnJumpInputUp ();
	}
}

Saltar a una plataforma desde abajo

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

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

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

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

Caer de la plataforma al pulsar abajo

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

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;
			}
Player.csvoid Update(){
	...
	//controller.Move(velocity * Time.deltaTime);
	controller.Move(velocity * Time.deltaTime, directionalInput);
	...
}

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.

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);
		}
	}
}

Mejorar el efecto de la cámara

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

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

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

Algunas mejoras

CameraFollow.cspublic float verticalOffset;

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

Mover la cámara a ambos lados del player

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;
}

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.

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;
}

Mover realmente la cámara

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

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.

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;
		}
	}
}

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):
      adb install -r \ruta-apk\prueba.apk

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:

//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);

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

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;
}

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

public ScrollRect scrollRect;

void Start () {
	scrollRect.horizontalNormalizedPosition = 1;	
}

Con ajuste repentino al moverme

void Update(){
	if(Input.GetMouseButtonUp(0)){
		snap ();
	}
}

void snap(){
	float valorActual = scrollRect.horizontalNormalizedPosition;
	valorActual = Mathf.Round (valorActual);
	scrollRect.horizontalNormalizedPosition = valorActual;
}

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.

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;
}

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

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;
	}
}
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;
	}   
}

La carta llama al GameController

Destapo dos cartas

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;
}
Card.cs private void flipCard(){
	// this.GetComponent<Image>().sprite = _cardFace;
	gc.evaluarCartas(this);
}

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

Guardar y recuperar información almacenada

PlayerPrefs.SetInt("valorNumerico", 3);
int v = PlayerPrefs.GetInt("valorNumerico");

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:

<div id="gameContainer" style="width: 640px; height: 360px"></div>

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

  •     Couldn't open /opt/Unity/Editor/Data/UnityExtensions/Unity/VR/Android/AudioPluginOculusSpatializer.so, error:
    			/opt/Unity/Editor/Data/UnityExtensions/Unity/VR/Android/AudioPluginOculusSpatializer.so: wrong ELF class: ELFCLASS32
    		
    Este error me dió en linux al compilar para Android. Se soluciona ejecutando le siguiente comando en la terminal:
    cd /opt/Unity/Editor/Data/UnityExtensions/Unity/VR/Android/ && sudo mv AudioPluginOculusSpatializer.so AudioPluginOculusSpatializer.ignore
    		
  • 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:

  • gratuito hasta los 20 UCC (Usuarios Conectados Simultáneamente). Si sobrepasas el límite, los nuevos usuarios no se podrán conectar.
  • Hay un límite de 500 mensajes por Room (partida).

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.

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);
}
}

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);
		}
	}
}

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 mailSOPORTE Usuarios Premium
Pablo Monteserín
contacta conmigoPablo Monteserín

Para dudas técnicas sobre los ejercicios de mis cursos es necesario tener una cuenta premium activa. Para cualquier otra cosa, puedes usar el formulario de la página de contacto.