[Diario de desarrollo del juego 01] Gestión de la interfaz de usuario y gestión de audio

Progreso de este artículo: cambio de interfaz y reproducción de audio.

0 Prefacio

Siempre siento que mi código no es lo suficientemente "hermoso". Para ampliar, algunas estructuras de datos, principios de diseño y patrones de diseño solo se conocen en teoría, pero no se pueden practicar bien en proyectos. Recientemente comencé a mirar algunos códigos fuente de dalao, aprender algunas ideas de desarrollo y consolidar conocimientos teóricos.

Actualizaré algo nuevo aprendido probablemente todos los días.

1 modo singleton

1.1 modo singleton

Uno de los patrones de diseño más utilizados en Unity es el patrón singleton. El patrón Singleton garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global para acceder a ella.

hay que tener en cuenta es:

  • Una clase singleton sólo puede tener una instancia.
  • Una clase singleton debe crear su propia instancia única.
  • La clase singleton debe proporcionar esta instancia a todos los demás objetos.

Los siguientes son tres patrones singleton comunes:

1.1.1 Singleton perezoso

public class SingletonClass{
    
    
	private static SingletonClass instance;
	//私有化构造函数,保证实例为单例类自己创建
	private SingletonClass(){
    
    }
	public static Singleton Instance{
    
    
		get{
    
    
			if(instance==null) instance = new SingletonClass();
			return instance;
		}
	}
}

La creación de instancias no se realiza hasta que el objeto solicita una instancia. Este enfoque se denomina singletonización diferida. La singletonización diferida evita la creación de instancias de singleton innecesarios cuando se inicia el programa y evita el desperdicio de memoria.

Sin embargo, el principal inconveniente de esta implementación es que no es segura en subprocesos múltiples. Si subprocesos de ejecución independientes ingresan al método de propiedad Instancia al mismo tiempo, se pueden crear múltiples instancias de objetos SingletonClass.

Hay muchas maneras de resolver este problema. Una es utilizar un modismo llamado bloqueo de doble verificación. Sin embargo, C# combinado con CLR (Common Language Runtime) proporciona un método de inicialización estático que puede evitar estos problemas sin necesidad de que los desarrolladores escriban explícitamente código seguro para subprocesos.

1.1.2 Singleton estilo Han hambriento

public sealed class SingletonClass{
    
    
	private static readonly SingletonClass instance = new SingletonClass();
	private SIngletonClass(){
    
    }
	public static SingletonClass Instance{
    
    
		get{
    
     return instance; }
	}
}

El método de inicialización estática también se denomina singletonización hambrienta . En este enfoque, se crea una instancia cuando se hace referencia a cualquier miembro de la clase por primera vez (en relación con la naturaleza de las variables estáticas). El CLR es responsable de la inicialización de variables. La clase está marcada como sellada para evitar la derivación, lo que puede aumentar las instancias. Además, la variable está marcada como de solo lectura, lo que significa que solo se le puede asignar un valor durante la inicialización estática (que se muestra aquí) o en el constructor de la clase.

1.1.3 Cerradura de doble verificación

La inicialización estática es factible en la mayoría de situaciones. Pero cuando su programa debe retrasar la creación de instancias, utilizar constructores no predeterminados o realizar otras tareas antes de la creación de instancias y trabajar en un entorno de subprocesos múltiples (hay situaciones en las que no se puede confiar en CLR para garantizar la seguridad de los subprocesos), necesita otras soluciones. .

Una solución es utilizar bloqueos de doble verificación para aislar subprocesos y evitar crear nuevas instancias del singleton al mismo tiempo.

public sealed class SingletonClass{
    
    
	private static volatile SingletonClass instance;
	private static object syncRoot = new Object();
	private SingletonClass(){
    
    }
	public static SingletonClass Instance{
    
    
		get{
    
    
			//先判断再加锁,避免频繁加锁造成性能消耗
			if(instance==null){
    
    
				lock(syncRoot){
    
    
					//加锁后再判断,避免在等待锁的过程中变量已被修改。
					if(instance==null) instance = new SingletonClass();
				}
			}
		}
	}
}

Este enfoque garantiza que solo se cree una instancia y solo cuando la instancia sea necesaria. Además, declare la variable como volátil (la palabra clave volátil indica que un campo puede ser modificado por varios subprocesos que se ejecutan simultáneamente) para garantizar que la asignación a la variable de instancia se complete antes de acceder a la variable de instancia y para resolver algunos reordenamientos de instrucciones causados ​​por el programa en ejecución pregunta. Finalmente, este método utiliza la instancia syncRoot para bloquear, en lugar de bloquear el tipo en sí, para evitar interbloqueos.

Este enfoque de bloqueo de doble verificación resuelve los problemas de concurrencia de subprocesos y al mismo tiempo evita la necesidad de utilizar un bloqueo exclusivo cada vez que se llama al método de propiedad de la instancia. También le permite retrasar la creación de instancias hasta que se acceda al objeto por primera vez. De hecho, los programas rara vez utilizan esta implementación. En la mayoría de los casos, los métodos de inicialización estáticos son suficientes.

1.2 Método de escritura singleton de Unity

Aquí hay dos formas de escribir patrones singleton comúnmente utilizados en Unity.

1.2.1 Singleton ordinario

Los singleton ordinarios se implementan en Awake(). En Unity, el método Awake () generalmente se llama antes que el método Start (), y generalmente obtenemos componentes de objetos o inicializamos campos y asignamos valores en Start (), por lo que implementamos la lógica principal del modo singleton en Awake ( ).

public class SingletonClass : MonoBehaviour{
    
    
	public static SingletonClass Instance;
	public void Awake(){
    
    
		if(Instance==null) Instance = this;
		else Destory(gameObject);
	}
}

1.2.2 Patrón singleton universal

Los genéricos se utilizan para crear plantillas de clases y, al permitir que las subclases hereden el patrón singleton, las subclases pueden implementar las funciones del patrón singleton.

//单例基类
public class SingletonBaseClass<T> : MonoBehaviour{
    
    
	public static T Instance;
	public void Awake(){
    
    
		if(Instance==null) Instance = this;
		else Destory(gameObject);    
	}
}
//单例子类
public class SingletonClass<T> : SingletonBaseClass<SingletonClass>{
    
    
	...
}

en

if(Instance==null) Instance = this;
else Destory(gameObject);

También se puede escribir como:

//返回SingletonClass类型第一个激活的加载的物体。
if(Instance==null) Instance = FindObjectOfType(typeof(SingletonClass)) as SingletonClass;

Sin embargo, cabe señalar que el código anterior no considera subprocesos múltiples. Cuando la clase se crea en diferentes subprocesos, el modo singleton de este método de escritura fallará.

2 delegado

2.1 Descripción general

Para realizar la parametrización del método, se propone el concepto de delegación. Un delegado es una clase, un tipo de referencia que puede apuntar a uno o más métodos. Lo que se almacena en la referencia del objeto delegado no es una referencia a los datos, sino una referencia al método. Los métodos almacenados requieren compatibilidad de tipos (es decir, los valores y parámetros de retorno son los mismos que los del delegado).

2.2 Delegación personalizada

2.2.1 Formato de declaración

public delegate void Mydelegate();  //该委托类型可以指向任何一个返回值为空,参数列表为空的其他方法。

2.2.2 Suscripción delegada

1. Delegación de unidifusión

La forma en que un delegado encapsula un método se llama [delegado de unidifusión].

mydelegate = new Mydelegate(Method);  //完整订阅格式
mydelegate = Method;    //简洁订阅格式,“=”可以用“+=”代替。
2. Delegación de multidifusión

Un delegado que encapsula múltiples métodos se llama [delegado de multidifusión].

mydelegate = MethodA;
mydelegate += MethodB;
mydelegate += MethodC;

Para los delegados de multidifusión, recuerde que solo la suscripción del primer método puede utilizar la operación de asignación (es decir, "="), y las suscripciones posteriores deben ser "+="; de lo contrario, las suscripciones posteriores sobrescribirán las suscripciones anteriores.

3. Ejemplo sencillo
public class Test : MonoBehaviour{
    
    
	public delegate int Mydelegate(int a,int b);  //嵌套类
	Mydelegate mydelegate;
	private void OnEnable(){
    
    
		mydelegate = Add;
		mydelegate += Mutiply;
	}
	public void Update(){
    
    
		if(Input.GetKeyDown(keyCode.Space)){
    
    
			Debug.Log(mydelegate(2,3));
		}
	}
	public int Add(int a,int b){
    
    
		Debug.Log(a+b);
		return a+b;
	}
	public int Mutiply(int a,int b){
    
    
		Debug.Log(a*b);
		return a*b;
	}
}

Producción:

5   //执行Add(2,3)
6   //执行Mutiply(2,3)
6   //执行Debug.Log(6)

2.3 Acción y función

Action<> y Func<> son tipos de delegados propios de C#.

2.3.1 Acción

El delegado de acción debe apuntar a un método sin valor de retorno y los parámetros son opcionales.

//声明
Action action;
Action<参数> action;

Ejemplo de código:

public class Test : MonoBehaviour{
    
    
	//指向一个无返回值,参数为string的方法
	Action<string> action;  
	//OnEnable和Awake/Start的区别在于,当挂载脚本的游戏物体被取消激活再重新激活的时候,脚本的Awake/Start都不会重新执行,而OnEnable会重新在第一帧执行一次。
	private void OnEnable(){
    
    
		action = SayHello;
	}
	void Update(){
    
    
		//用户按下空格时
		if(Input.GetKeyDown(keyCode.Space){
    
    
			action("Ben");   //or action.Invoke("Ben");
		}
	}
	private void SayHello(string name){
    
    
		Debug.Log("Hello~"+name);
	}
}

2.3.2 Función

Func<> debe apuntar a un método con un valor de retorno y los parámetros son opcionales.

//声明
Func<返回值> func;
Func<参数,参数,返回值> func;

2.4 Uso común

2.4.1 Método de plantilla

Hay una incertidumbre y el resto del código está fijo y escrito. Esta parte incierta (parámetros) se completa con los métodos contenidos en los parámetros de tipo delegado que pasamos. Dado que este método generalmente necesita devolver un valor, el delegado Func generalmente se usa como método de plantilla.

Ejemplo: este ejemplo muestra el nombre del jugador con el mayor número de banderas, muertes y asesinatos.

1. Definir categorías de información básica del jugador.

public class PlayerStatus{
    
    
	public string playerName;
	public int killNum,flagNum,deathNum;
}

2. Declare un tipo de delegado.

public delegate int GetTopScoreDelegate(PlayerStatus player);

3. Cree métodos que sean compatibles con el tipo de delegado.

public int GetKillNum(PlayerStatus player){
    
    
	return player.killNum;
}
public int GetFlagNum(PlayerStatus player){
    
    
	return player.flagNum;
}
public int GetDeathNum(PlayerStatus player){
    
    
	return player.deathNum;
}

4. Cree un método que tome el tipo de delegado como parámetro.

public string GetTopName(GetTopScoreDelegate _delegate){
    
    
	int topNum = 0;
	string name = "";
	foreach(PlayerStatus player in playerStatuses){
    
    
		int tempNum = _delegate(player);
		if(tempNum>topNum){
    
    
			topNum = tempNum;
			name = player.playerName;
		}
	}
	return name;
}

5. Uso.

public void Start(){
    
    
	topKillName = GetTopName(GetKillName);
	topFlagName = GetTopName(GetFlagName);
	topDeathName = GetTopName(GetDeathName);
	Debug.Log(topKillName+" "+topFlagName+" "+topDeathName);
}

6. Suplemento

Puede utilizar funciones anónimas de expresiones Lambda como parámetros del método, por lo que se puede omitir el cuarto paso.

Expresión lambda: (nombre del parámetro de entrada) => devolver el valor devuelto.

public void Start(){
    
    
	topKillName = GetTopName((player)=>player.killNum);
	topFlagName = GetTopName((player)=>player.flagNum);
	topDeathName = GetTopName((player)=>player.deathNum);
	Debug.Log(topKillName+" "+topFlagName+" "+topDeathName);
}

2.4.2 Método de devolución de llamada

Utilice un delegado en forma de método de devolución de llamada y elija dinámicamente si llamarlo según la lógica.

ejemplo:

public class Test : MonoBehaviour{
    
    
	public Box WrapProduct(Func<Product> _func,Action<Product> _action){
    
    
		Box box = new Box();
		box.Product = _func();
		if(_func().Price>5) _action(_func());
		return box;
	}
}

Los parámetros de este método incluyen un delegado Fuc<> cuyo valor de retorno es el tipo de Producto y un delegado Acción<> cuyo parámetro es el tipo de Producto. El valor de retorno del delegado de función se asigna a box.Product. Si el precio del valor de retorno de func() es mayor que 5, utilice el valor de retorno del tipo de producto devuelto por func como parámetro del delegado de acción. Si el precio del valor de retorno de func () no es mayor que 5, entonces nunca se llamará al delegado de acción, por lo que la acción se llama a través de la lógica interna, por lo que el método encapsulado por el delegado de acción aquí se usa como método de devolución de llamada. Luego, el método encapsulado por el delegado Func se utiliza como método de plantilla.

3 practica

Preparación: cree la página y guárdela como una casa prefabricada.

interfaz de usuario prefabricada

3.1 Manejo de relaciones de interfaz

3.1.1 UIBase

UIBase sirve como clase base de interfaz y todas las clases de interfaz deben heredar de UIBase.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 界面基类
/// </summary>
public class UIBase : MonoBehaviour
{
    
    
    //方法1:显示
    public virtual void Show() {
    
    
        gameObject.SetActive(true);
    }
    
    //方法2:隐藏
    public virtual void Hide() {
    
    
        gameObject.SetActive(false);
    }

    //方法3:销毁
    public virtual void Close() {
    
    
        UIManager.Instance.CloseUI(gameObject.name);
    }
}

3.1.2 Administrador de UI

Como clase de administración de interfaz, UIManager administra varias interfaces de manera uniforme y utiliza el modo singleton.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 界面管理器
/// </summary>
public class UIManager : MonoBehaviour
{
    
    
    public static UIManager Instance;    //单例
    public List<UIBase> uiList;          //容器:存储界面
    private Transform canvasTF;          //canvas物体作为界面物体的父物体,transform一般用来描述父子关系
    public void Awake() {
    
    
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
        uiList = new List<UIBase>();
        canvasTF = GameObject.Find("Canvas").transform;
    }

    //方法1:显示(泛型约束,T需要继承自UIBase
    public UIBase ShowUI<T>(string uiName) where T : UIBase {
    
    
        UIBase ui = Find(uiName);
        //集合中没有,需要从Resources/UI文件夹中加载
        if (ui == null) {
    
    
            //Instantiate(Object original,Transform parent); 
            //original:要实例化的物体; parent:实例化的物体将作为该物体的子对象
            //PS:Transform可用来指定对象的父子关系,如A物体的trasnform类a是游戏物体B的transform类b的父类的话,物体A也是物体B的父类。
            GameObject obj = Instantiate(Resources.Load("UI/" + uiName), canvasTF) as GameObject;
            obj.name = uiName;           //改名字
            ui = obj.AddComponent<T>();  //添加需要的脚本
            uiList.Add(ui);              //添加到集合进行存储
        }
        //显示
        else {
    
    
            ui.Show();
        }
        return ui;
    }

    //方法2:隐藏
    public void HideUI(string uiName) {
    
    
        UIBase ui = Find(uiName);
        if (ui != null) {
    
    
            ui.Hide();
        }
    }


    //方法3:关闭
    //3.1 关闭所有界面
    public void CloseAllUI() {
    
    
        for (int i = uiList.Count - 1; i >= 0; i--) {
    
    
            Destroy(uiList[i].gameObject);
        }
        uiList.Clear();
    }
    //3.2 关闭某个界面
    public void CloseUI(string uiName) {
    
    
        UIBase ui = Find(uiName);
        if (ui != null) {
    
    
            uiList.Remove(ui);
            Destroy(ui.gameObject);
        }
    }

    //辅助方法1:从集合中找到名字对应的界面
    public UIBase Find(string uiName) {
    
    
        for (int i = 0; i < uiList.Count; i++) {
    
    
            if (uiList[i].name == uiName) {
    
    
                return uiList[i];
            }
        }
        return null;
    }
}

3.2 Monitoreo de eventos

Aquí, haz clic en [Iniciar juego] para salir de la interfaz de inicio.

3.2.1 Activador de evento UI

using UnityEngine;
using UnityEngine.EventSystems;
using System;
/// <summary>
/// 事件监听
/// </summary>
public class UIEventTrigger : MonoBehaviour,IPointerClickHandler
{
    
    
    //Action:Unity自带委托,PointerEventData:鼠标点击事件
    public Action<GameObject, PointerEventData> onClick;
    
    //方法1:给物体加UIEventTrigger脚本
    public static UIEventTrigger Get(GameObject obj) {
    
    
        UIEventTrigger trigger = obj.GetComponent<UIEventTrigger>();
        if (trigger == null) {
    
    
            trigger = obj.AddComponent<UIEventTrigger>();
        }
        return trigger;
    }

    //方法2:UI对应的鼠标点击事件,触发时调用委托
    public void OnPointerClick(PointerEventData eventData) {
    
    
        if (onClick != null) {
    
    
            onClick(gameObject, eventData);
        }
    }
}

3.2.2 UIBase

Se agregó un método de registro de eventos.

//方法0:注册事件
public UIEventTrigger Register(string name) {
    
    
	Transform tf = transform.Find(name);           //transform.Find(string):找子辈物体
	return UIEventTrigger.Get(tf.gameObject);      //给物体加UIEventTrigger脚本
}

3.2.3 UI de inicio de sesión

Inicie el script de la interfaz.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 开始界面
/// </summary>
public class LoginUI : UIBase {
    
    
    private void Awake() {
    
    
        //开始游戏:Register("bg/startBtn"):给物体加EventTrigger脚本,.onClick=onStartGameBtn:委托的方法参数。
        Register("bg/startBtn").onClick = OnStartGameBtn;
    }
    private void OnStartGameBtn() {
    
    
        //关闭login界面
        Close();
    }
}

3.2.4 Aplicación de juego

Como script de comando global, también se puede considerar como la entrada de inicio.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 游戏入口脚本
/// </summary>
public class GameApp : MonoBehaviour
{
    
    
    // Start is called before the first frame update
    void Start()
    {
    
    
        //显示loginUI界面
        UIManager.Instance.ShowUI<LoginUI>("LoginUI");
    }

}

3.3 Reproducción de audio

3.3.1 Uso de PlayClipAtPoint()

Los sonidos del juego generalmente se pueden dividir en música y efectos de sonido: la música suele ser más larga y debe reproducirse en un bucle, mientras que los efectos de sonido son más cortos en tiempo y no es necesario reproducirlos en un bucle.

Hay dos formas comunes de reproducir sonidos:

1. Cree un objeto vacío, agregue AudioSource para cada música o efecto de sonido y agregue los clips de sonido correspondientes a cada AudioSource. Al reproducir, obtenga cada AudioSource para reproducir el sonido.

2. Cree un objeto vacío, agregue un AudioSource para reproducir música de fondo y luego agregue un AudioSource para reproducir efectos de sonido. La propiedad AudioClip de este componente asigna diferentes clips de sonido según los sonidos que deben reproducirse en tiempo de ejecución, lo que permite que un AudioSource reproduzca múltiples efectos de sonido. Pero hay un problema con este método: cuando es necesario reproducir varios efectos de sonido al mismo tiempo, el efecto de sonido reproducido más tarde finalizará el efecto de sonido reproducido primero. Solución: agrupe todos los efectos de sonido en un grupo y agregue un AudioSource a cada grupo de efectos de sonido.

Pero hay otra manera:

static void PlayClipAtPoint(AudioClip clip,Vector3 position,float volume = 1.0F);

El uso de AudioSource.PlayClipAtPoint para reproducir un sonido generará automáticamente un objeto llamado "One shot audio" y agregará automáticamente AudioSource y el AudioClip correspondiente. La reproducción de varios sonidos al mismo tiempo generará varios objetos con el mismo nombre. La reproducción de cada sonido no se afectarán entre sí Desventajas Solo puede configurar el volumen y la posición, pero no el bucle. Una vez completada la reproducción, el audio One Shot se destruye automáticamente.

De esta manera se realiza la siguiente operación.

3.3.2 Administrador de audio

Gestor de audio.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 声音管理器
/// </summary>

public class AudioManager : MonoBehaviour
{
    
    
    public static AudioManager Instance;   //单例模式
    public AudioSource bgmSource;          //播放bgm的音频
    public void Awake() {
    
    
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

    //方法1:初始化
    public void Init() {
    
    
        bgmSource = gameObject.AddComponent<AudioSource>();
    }

    //方法2:播放bgm
    public void PlayBGM(string name,bool isLoop = true) {
    
    
        AudioClip clip = Resources.Load<AudioClip>("Sounds/BGM/"+name);
        bgmSource.clip = clip;
        bgmSource.loop = isLoop;
        bgmSource.Play();
    }

    //方法3:播放音效
    public void PlayEffect(string name) {
    
    
        AudioClip clip = Resources.Load<AudioClip>("Sounds/" + name);
        AudioSource.PlayClipAtPoint(clip, transform.position);
    }
}

3.3.3 Aplicación de juego

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 游戏入口脚本
/// </summary>
public class GameApp : MonoBehaviour
{
    
    
    // Start is called before the first frame update
    void Start()
    {
    
    
        //1.显示loginUI界面
        UIManager.Instance.ShowUI<LoginUI>("LoginUI");
        //2.初始化音频
        AudioManager.Instance.Init();
        AudioManager.Instance.PlayBGM("bgm1");
    }

}

4 resultados

Ingrese a la interfaz de inicio, reproduzca BGM y haga clic en [Iniciar juego] para ingresar a la interfaz del juego.

Iniciar sesión UI
interfaz del juego

Supongo que te gusta

Origin blog.csdn.net/manpi/article/details/129233745
Recomendado
Clasificación