[Game Development Diary 01] UI interface management and audio management

Progress of this article: interface switching & audio playback.

0 Preface

I always feel that my code is not "beautiful" enough. To expand, some data structures, design principles, and design patterns are only known in theory, but cannot be well practiced in projects. So recently I started to look at some dalao source code, learn some development ideas, and consolidate theoretical knowledge.

I will update something new learned probably every day.

1 Singleton mode

1.1 Singleton pattern

One of the most used design patterns in Unity is the singleton pattern. The Singleton pattern ensures that a class has only one instance and provides a global access point to access it.

have to be aware of is:

  • A singleton class can only have one instance.
  • A singleton class must create its own unique instance.
  • The singleton class must provide this instance to all other objects.

The following are three common singleton patterns:

1.1.1 Lazy Singleton

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

Instantiation is not performed until the object requests an instance. This approach is called lazy singletonization. Lazy singletonization avoids instantiating unnecessary singletons when the program starts and avoids memory waste.

However, the main drawback of this implementation is that it is not safe under multi-threading. If independent execution threads enter the Instance property method at the same time, multiple instances of SingletonClass objects may be created.

There are many ways to solve this problem. One is to use an idiom called double-check locking. However, C# combined with the CLR (Common Language Runtime) provides a static initialization method that can avoid these problems without requiring developers to explicitly write thread-safe code.

1.1.2 Hungry Han style singleton

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

The static initialization method is also called hungry singletonization . In this approach, an instance is created when any member of the class is referenced for the first time (related to the nature of static variables). The CLR is responsible for variable initialization. The class is marked sealed to prevent derivation, which may increase instances. Additionally, the variable is marked readonly, which means it can only be assigned a value during static initialization (shown here) or in the class constructor.

1.1.3 Double check lock

Static initialization is feasible for most situations. But when your program must delay instantiation, use non-default constructors or perform other tasks before instantiation, and work in a multi-threaded environment (there are situations where the CLR cannot be relied upon to ensure thread safety), you need other solutions. .

One solution is to use double-checked locks to isolate threads and avoid creating new instances of the singleton at the same time.

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

This approach ensures that only one instance is created, and only when the instance is needed. In addition, declare the variable as volatile (the volatile keyword indicates that a field can be modified by multiple threads executing simultaneously) to ensure that the assignment to the instance variable is completed before accessing the instance variable, and to solve some instruction rearrangements caused by the program running. question. Finally, this method uses the syncRoot instance to lock, rather than locking the type itself, to avoid deadlocks.

This double-checked lock approach solves thread concurrency issues while avoiding the need to use an exclusive lock every time the Instance property method is called. It also allows you to delay instantiation until the object is first accessed. In fact, programs rarely use this implementation. In most cases, static initialization methods are sufficient.

1.2 Unity singleton writing method

Here are two ways of writing singleton patterns commonly used in Unity.

1.2.1 Ordinary singleton

Ordinary singletons are implemented in Awake(). In Unity, the Awake() method is generally called before the Start() method, and we usually obtain object components or initialize fields and assign values ​​in Start(), so we implement the main logic of the singleton mode in Awake().

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

1.2.2 Universal singleton pattern

Generics are used to template classes, and by letting subclasses inherit the singleton pattern, the subclasses can implement the functions of the singleton pattern.

//单例基类
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>{
    
    
	...
}

in

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

It can also be written as:

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

However, it should be noted that the above code does not consider multi-threading. When the class is created in different threads, the singleton mode of this writing method will fail.

2 delegate

2.1 Overview

In order to realize method parameterization, the concept of delegation is proposed. A delegate is a class, a reference type that can point to one or more methods. What is stored in the reference of the delegate object is not a reference to the data, but a reference to the method. Stored methods require type compatibility (i.e. return values ​​and parameters are the same as the delegate).

2.2 Custom delegation

2.2.1 Declaration format

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

2.2.2 Delegated subscription

1. Unicast delegation

The form in which a delegate encapsulates a method is called [unicast delegate].

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

A delegate encapsulating multiple methods is called [multicast delegation].

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

For multicast delegates, remember that only the subscription of the first method can use the assignment operation (i.e. "="), and subsequent subscriptions must be "+=", otherwise subsequent subscriptions will overwrite previous subscriptions.

3. Simple example
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;
	}
}

Output:

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

2.3 Action&Func

Action<> and Func<> are C#’s own delegate types.

2.3.1 Action

The Action delegate must point to a method with no return value, and the parameters are optional.

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

Code example:

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 Func

Func<> must point to a method with a return value, and the parameters are optional.

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

2.4 Common usage

2.4.1 Template method

There is one uncertainty, and the rest of the code is fixed and written. This uncertain part (parameters) is filled by the methods contained in the delegate type parameters we pass in. Since this method generally needs to return a value, the Func delegate is generally used as the template method.

Example: This example is to output the name of the player with the highest number of flags, deaths and kills.

1. Define basic player information categories.

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

2. Declare a delegate type.

public delegate int GetTopScoreDelegate(PlayerStatus player);

3. Create methods that are compatible with the delegate type.

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. Create a method that takes the delegate type as a parameter.

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

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

6. Supplement

You can use Lambda expression anonymous functions as method parameters, so the fourth step can be omitted.

Lambda expression: (parameter name of the input parameter) =>return the returned value.

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 Callback method

Use a delegate in the form of a callback method and dynamically choose whether to call it based on logic.

example:

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

The parameters of this method include a Fuc<> delegate whose return value is Product type and an Action<> delegate whose parameter is Product type. The return value of func delegate is assigned to box.Product. If the Price of the return value of func() is greater than 5, use the Product type return value returned by func as a parameter of the Action delegate. If the Price of the return value of func() is not greater than 5, then the Action delegate will never be called, so Action is called through internal logic, so the method encapsulated by the Action delegate here is used as a callback method, then The method encapsulated by the Func delegate is used as a template method.

3 Practice

Preparation: Build the page and store it as a prefab.

UI prefab

3.1 Handling interface relationships

3.1.1 UIBase

UIBase serves as the interface base class, and all interface classes need to inherit from 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 UIManager

As an interface management class, UIManager manages various interfaces uniformly and uses singleton mode.

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 Event monitoring

Here, click [Start Game] to jump out of the start interface.

3.2.1 UIEventTrigger

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

Added an event registration method.

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

3.2.3 LoginUI

Start interface script.

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 GameApp

As a global command script, it can also be considered as the startup entrance.

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 Audio playback

3.3.1 Usage of PlayClipAtPoint()

The sounds in the game can usually be divided into music and sound effects. Music is usually longer and needs to be played in a loop, while sound effects are shorter in time and does not need to be played in a loop.

There are two common ways to play sounds:

1. Create an empty object, add AudioSource for each music or sound effect, and add corresponding sound clips to each AudioSource. When playing, obtain each AudioSource to play the sound.

2. Create an empty object, add an AudioSource to play background music, and then add an AudioSource to play sound effects. The AudioClip property of this component assigns different sound clips according to the sounds that need to be played at runtime, thereby enabling one AudioSource to play multiple sound effects. But there is a problem with this method: when multiple sound effects need to be played at the same time, the sound effect played later will terminate the sound effect played first. Solution: Group all sound effects into one group, and add an AudioSource to each group of sound effects.

But there is another way:

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

Using AudioSource.PlayClipAtPoint to play a sound will automatically generate an object named "One shot audio", and automatically add AudioSource and the corresponding AudioClip. Playing multiple sounds at the same time will generate multiple objects with the same name. The playback of each sound will not affect each other. Disadvantages You can only set the volume and position, but not the loop. After playback is completed, One shot audio is automatically destroyed.

The following operation is done in this way.

3.3.2 AudioManager

Audio manager.

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 GameApp

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 results

Enter the start interface, play BGM, and click [Start Game] to enter the game interface.

LoginUI
game interface

Guess you like

Origin blog.csdn.net/manpi/article/details/129233745