【ゲーム開発日記01】UIインターフェース管理とオーディオ管理

この記事の進捗: インターフェイスの切り替えとオーディオの再生。

0 まえがき

私は自分のコードが十分に「美しく」ないといつも感じています。さらに詳しく説明すると、一部のデータ構造、設計原則、設計パターンは理論的にのみ知られていますが、プロジェクトでは十分に実践することができません。そこで最近、dalao のソース コードをいくつか見て、開発アイデアを学び、理論的な知識を強化し始めました。

おそらく毎日新しく学んだことを更新します。

1 シングルトンモード

1.1 シングルトンモード

Unity で最もよく使用されるデザイン パターンの 1 つは、シングルトン パターンです。シングルトン パターンでは、クラスにインスタンスが 1 つだけ存在することが保証され、そのインスタンスにアクセスするためのグローバル アクセス ポイントが提供されます。

注意しなければならないことは次のとおりです。

  • シングルトン クラスはインスタンスを 1 つだけ持つことができます。
  • シングルトン クラスは、独自の一意のインスタンスを作成する必要があります。
  • シングルトン クラスは、このインスタンスを他のすべてのオブジェクトに提供する必要があります。

一般的なシングルトン パターンは次の 3 つです。

1.1.1 遅延シングルトン

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

オブジェクトがインスタンスを要求するまでインスタンス化は実行されません。このアプローチは遅延シングルトン化と呼ばれます。遅延シングルトン化により、プログラムの開始時に不要なシングルトンがインスタンス化されることが回避され、メモリの無駄が回避されます。

ただし、この実装の主な欠点は、マルチスレッド環境では安全ではないことです。独立した実行スレッドが同時に Instance プロパティ メソッドに入ると、SingletonClass オブジェクトの複数のインスタンスが作成される可能性があります。

この問題を解決する方法はたくさんあります。1 つは、ダブルチェック ロックと呼ばれる慣用句を使用することです。ただし、C# と CLR (共通言語ランタイム) を組み合わせると、開発者がスレッド セーフなコードを明示的に作成しなくても、これらの問題を回避できる静的初期化メソッドが提供されます。

1.1.2 ハングリーハンスタイルシングルトン

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

静的初期化メソッドは、 ハングリー シングルトン化 とも呼ばれますこのアプローチでは、クラスのメンバーが初めて参照されるときにインスタンスが作成されます (静的変数の性質に関連します)。CLR は変数の初期化を担当します。このクラスは派生を防ぐためにシールされているため、インスタンスが増加する可能性があります。さらに、変数には読み取り専用のマークが付けられます。これは、静的初期化中 (ここに表示) またはクラス コンストラクター内でのみ値を割り当てることができることを意味します。

1.1.3 ダブルチェックロック

静的初期化は、ほとんどの状況で実行可能です。ただし、プログラムでインスタンス化を遅らせる必要がある場合、デフォルト以外のコンストラクターを使用するか、インスタンス化の前に他のタスクを実行する必要がある場合、およびマルチスレッド環境で動作する場合 (スレッド セーフを確保するために CLR に依存できない状況があります)、他のソリューションが必要になります。 。

解決策の 1 つは、二重チェックされたロックを使用してスレッドを分離し、同時にシングルトンの新しいインスタンスが作成されるのを避けることです。

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

このアプローチにより、インスタンスが必要なときにのみ、インスタンスが 1 つだけ作成されるようになります。さらに、変数を volatile として宣言し (volatile キーワードは、同時に実行する複数のスレッドによってフィールドを変更できることを示します)、インスタンス変数への割り当てがインスタンス変数にアクセスする前に完了していることを確認し、インスタンス変数によって引き起こされる一部の命令の再配置を解決します。実行中のプログラムに関する質問です。最後に、このメソッドはデッドロックを回避するために、型自体をロックするのではなく、syncRoot インスタンスを使用してロックします。

この二重チェックされたロックのアプローチにより、スレッドの同時実行性の問題が解決され、インスタンス プロパティ メソッドが呼び出されるたびに排他ロックを使用する必要がなくなります。また、オブジェクトが最初にアクセスされるまでインスタンス化を遅らせることもできます。実際、プログラムがこの実装を使用することはほとんどありません。ほとんどの場合、静的な初期化メソッドで十分です。

1.2 Unityシングルトンの書き方

Unity で一般的に使用されるシングルトン パターンを記述する 2 つの方法を次に示します。

1.2.1 通常のシングルトン

通常のシングルトンは Awake() で実装されます。Unityでは通常、Awake()メソッドはStart()メソッドの前に呼び出され、通常はStart()でオブジェクトのコンポーネントを取得したり、フィールドを初期化して値を代入したりするため、シングルトンモードのメインロジックはAwake()で実装します。 )。

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

1.2.2 ユニバーサルシングルトンパターン

ジェネリックはクラスのテンプレート化に使用され、サブクラスにシングルトン パターンを継承させることで、サブクラスはシングルトン パターンの機能を実装できます。

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

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

次のように書くこともできます。

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

ただし、上記のコードはマルチスレッドを考慮していないため、クラスが異なるスレッドで作成された場合、このメソッドのシングルトン モードは失敗します。

2 代表者

2.1 概要

メソッドのパラメータ化を実現するために、委任の概念が提案されています。デリゲートはクラスであり、1 つ以上のメソッドを指すことができる参照型です。デリゲート オブジェクトの参照に格納されるのは、データへの参照ではなく、メソッドへの参照です。ストアド メソッドには型の互換性が必要です (つまり、戻り値とパラメーターがデリゲートと同じです)。

2.2 カスタム委任

2.2.1 宣言フォーマット

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

2.2.2 委任されたサブスクリプション

1. ユニキャスト委任

デリゲートがメソッドをカプセル化する形式は [ユニキャスト デリゲート] と呼ばれます。

mydelegate = new Mydelegate(Method);  //完整订阅格式
mydelegate = Method;    //简洁订阅格式,“=”可以用“+=”代替。
2. マルチキャスト委任

複数のメソッドをカプセル化したデリゲートを「マルチキャストデリゲーション」と呼びます。

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

マルチキャスト デリゲートの場合、最初のメソッドのサブスクリプションのみが割り当て操作 (つまり、「=」) を使用できることに注意してください。後続のサブスクリプションは「+=」である必要があります。そうしないと、後続のサブスクリプションが以前のサブスクリプションを上書きします。

3. 簡単な例
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;
	}
}

出力:

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

2.3 アクション&機能

Action<> と Func<> は C# 独自のデリゲート型です。

2.3.1 アクション

Action デリゲートは戻り値のないメソッドを指す必要があり、パラメーターはオプションです。

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

コード例:

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<返回值> func;
Func<参数,参数,返回值> func;

2.4 一般的な使用法

2.4.1 テンプレートメソッド

不確実な点が 1 つあり、コードの残りの部分は修正されて記述されます。この不確実な部分 (パラメーター) は、渡したデリゲート型パラメーターに含まれるメソッドによって埋められます。このメソッドは通常、値を返す必要があるため、通常は Func デリゲートがテンプレート メソッドとして使用されます。

例: この例は、フラグ数、デス数、キル数が最も多いプレイヤーの名前を出力する例です。

1. 基本的なプレーヤー情報のカテゴリを定義します。

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

2. デリゲートの型を宣言します。

public delegate int GetTopScoreDelegate(PlayerStatus player);

3. デリゲート型と互換性のあるメソッドを作成します。

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. デリゲート型をパラメータとして受け取るメソッドを作成します。

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. 使用します。

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

6. 補足

ラムダ式の匿名関数をメソッドのパラメータとして使用できるため、4 番目の手順を省略できます。

ラムダ式: (入力パラメータのパラメータ名) =>戻り値を返します。

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 コールバックメソッド

コールバック メソッドの形式でデリゲートを使用し、ロジックに基づいてデリゲートを呼び出すかどうかを動的に選択します。

例:

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

このメソッドのパラメータには、戻り値がProduct タイプである Fuc<> デリゲートと、パラメータが Product タイプである Action<> デリゲートが含まれます。func デリゲートの戻り値は box.Product に割り当てられます。戻り値の Price の場合、 func() の値が 5 より大きい場合は、func によって返される Product タイプの戻り値をアクション デリゲートのパラメーターとして使用します。func() の戻り値の Price が 5 以下の場合、アクション デリゲートは呼び出されないため、アクションは内部ロジックを通じて呼び出されるため、ここでアクション デリゲートによってカプセル化されたメソッドがコールバック メソッドとして使用されます。 、その後、Func デリゲートによってカプセル化されたメソッドがテンプレート メソッドとして使用されます。

3 練習

準備: ページを構築し、プレハブとして保存します。

UIプレハブ

3.1 インターフェース関係の処理

3.1.1 UIBase

UIBase はインターフェイスの基本クラスとして機能し、すべてのインターフェイス クラスは 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

UIManager はインターフェイス管理クラスとして、さまざまなインターフェイスを均一に管理し、シングルトン モードを使用します。

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 イベント監視

ここで、[ゲームを開始]をクリックして、開始インターフェイスを抜けます。

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

イベント登録方法を追加しました。

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

3.2.3 ログインUI

インターフェーススクリプトを開始します。

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 ゲームアプリ

グローバルコマンドスクリプトとして、起動の入り口とも言えます。

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 オーディオの再生

3.3.1 PlayClipAtPoint()の使用法

ゲーム内のサウンドは通常、音楽と効果音に分類できます。通常、音楽は長くてループ再生する必要があり、一方、効果音は時間が短く、ループで再生する必要はありません。

サウンドを再生するには 2 つの一般的な方法があります。

1. 空のオブジェクトを作成し、音楽や効果音ごとに AudioSource を追加し、対応するサウンド クリップを各 AudioSource に追加します。再生する場合は、各 AudioSource を取得してサウンドを再生します。

2. 空のオブジェクトを作成し、BGM を再生する AudioSource を追加してから、効果音を再生する AudioSource を追加します。このコンポーネントの AudioClip プロパティは、実行時に再生する必要があるサウンドに応じて異なるサウンド クリップを割り当てるため、1 つの AudioSource で複数のサウンド効果を再生できます。しかし、この方法には問題があります。複数の効果音を同時に再生する必要がある場合、後で再生される効果音によって最初に再生された効果音が終了してしまいます。解決策: すべてのサウンドエフェクトを 1 つのグループにグループ化し、オーディオソースをサウンドエフェクトの各グループに追加します。

しかし、別の方法もあります。

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

AudioSource.PlayClipAtPoint を使用してサウンドを再生すると、「ワンショット オーディオ」という名前のオブジェクトが自動的に生成され、AudioSource と対応する AudioClip が自動的に追加されます。複数のサウンドを同時に再生すると、同じ名前のオブジェクトが複数生成されます。各サウンドの再生デメリット 音量と位置のみ設定でき、ループは設定できません。再生が完了すると、ワンショット オーディオは自動的に破棄されます。

以下の動作はこのようにして行われる。

3.3.2 オーディオマネージャー

オーディオマネージャー。

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 ゲームアプリ

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 件の結果

スタートインターフェースに入り、BGMを再生し、[ゲーム開始]をクリックしてゲームインターフェースに入ります。

ログインUI
ゲームインターフェイス

おすすめ

転載: blog.csdn.net/manpi/article/details/129233745