Commonly used singleton patterns in Unity, script templates for object pools, double-click to exit, slide to turn pages or zoom in and out, and flexible use of attributes in code

1. Script template for singleton mode:

Unity can use singleton mode for some commonly used managers for unified function management:

//普通单例,不需要继承MonoBehavior,不用挂载在GameObject上
public class CommonSingletonTemplate<T> where T : class, new()
{
    private static readonly object sysLock = new object();
    private static T _instance;
    public static T GetInstance()
    {
        if (_instance == null)
        {
            lock (sysLock)
            {
                _instance = new T();
            }
        }
        return _instance;
    }
}


//使用该单例模式可以快速达到manager效果:
public class RewardedAdMgr : CommonSingletonTemplate<RewardedAdMgr>
{
    ........
    ........
}

However, for some singleton classes in Unity that need to be mounted on the GameObject as a Component, the following settings need to be made:

//该类型的单例需要继承MonoBehavior,并且操作逻辑也与一般的单例不同
public class UnitySingletonTemplate<T> : MonoBehaviour where T : Component
{
    private static readonly object sysLock = new object();
    private static T _instance;
    public static T GetInstance()
    {
        if (_instance == null)
        {
            lock (sysLock)
            {
                _instance = GameObject.FindObjectOfType<T>() as T;
                if(_instance == null)
                {
                    GameObject obj = GameObject.Find("GameMgr");       
                    //这个统一管理的GameObject应该始终为true状态,所以使用GameObject.Find是能够查找到的
                    //如果gameObject本身为false状态,则在脚本中使用GameObject.Find是无法查找到该gameObject的,查找结果为null
                    if (obj == null)
                        obj = new GameObject("GameMgr");
                    _instance = obj.AddComponent<T>();
                }
            }
        }
        return _instance;
    }
}


//“GameMgr”作为整个游戏的Manager,某些方法需要在Awake/Start/Update中执行,所以需要继承自MonoBehavior
public class GameMgr : UnitySingletonTemplate<GameMgr>
{
    .........
    .........
}

PS:

1. Awake/Start/Update is not a method in MonoBehavior, why must inherit MonoBehavior?

Analysis: There is a "Message System" in the Unity engine. MonoBehavior is used to monitor Unity's message system . When certain specified events are triggered, all Components that inherit MonoBehavior and are mounted on GameObject will respond to the event . Call different methods in this class according to different event types, such as: Awake, Update, OnApplicationQuit/Pause, OnCollisionEnter, OnMouseDrag, OnDestroy, OnDisable, etc. For details, you can query the "Messages" module: Unity - Scripting API: MonoBehaviour

If there is no called method such as OnCollisionEnter, OnMouseDrag, OnApplicationQuit corresponding to event B such as "collision, dragging, program exit" in script A, then the script A will not respond to event B.

These methods such as Awake, Start, Update, OnCollisionEnter, etc. are not built-in methods in MonoBehavior. When declaring the method in the script, there is no "override" modifier before the method , and there is also the "private" keyword:

参考链接:c# - How are methods like Awake, Start, and Update called in Unity? - Game Development Stack Exchange

c# - In Unity what exactly is going on when I implement Update(), and other messages from MonoBehaviour - Stack Overflow

2. To mount the script on the GameObject, the script must inherit from MonoBehavior instead of Component

2. Object pool template:

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Events;

public class ObjectPool<T> where T : new()
{
    //除了构造方法,无法在外部再修改该值,但可以修改引用类型对象的内容本身,即可以对m_stack中元素进行出栈/入栈等操作
    private readonly Stack<T> m_stack = new Stack<T>();
    private readonly UnityAction<T> m_ActionGet;
    private readonly UnityAction<T> m_ActionRelease;

    #region 池子中T对象的数量统计
    private int m_totalCount = 0;
    public int totalCount
    {
        get { return m_totalCount; }
        private set { m_totalCount = value; }    //禁止被外部修改T总数量,保护数据的安全性
    }
    //问题:为什么需要统计“totalCount”?
    //解答:有些情况下需要统计游戏中当前已经使用的T对象的数量,此时不可能使用GameObject.Find来查找,太浪费资源
    //      故这里直接统计总的T对象数量,通过与m_stack数量的对比即可知道
    //      并且通过totalCount也可以控制T对象的总数量,便于内存管理
    public int inactiveCount       //无法直接修改池子中T对象总数量,因为是由m_stack自动生成的。
    {                           //提供该属性的目的在于某些情况下可能会需要m_stack中的T对象数量
        get { return m_stack.Count; }    //池子中可以使用的T对象总数量
    }
    public int activeCount { get { return totalCount - inactiveCount; } }   //获取场景中正在使用的T对象数量
    #endregion

    //泛型的构造方法,注意:“ObjectPool”后没有“<T>”
    public ObjectPool(UnityAction<T> actionGet, UnityAction<T> actionRelease)
    {
        m_ActionGet = actionGet;
        m_ActionRelease = actionRelease;
    }
    
    //获取对象池中某个对象的方法
    public T Get()
    {
        T item;
        if (m_stack.Count == 0)
        {
            item = new T();
            ++totalCount;     //由于private set,只可以在本类内部访问。基于private特性,即使是派生类也无法访问同一变量
        }
        else
            item = m_stack.Pop();

        //执行委托:
        m_ActionGet?.Invoke(item);    //使用可空修饰符

        return item;
    }

    //释放某个active item,将其放入对象池中
    public void Release(T item)
    {
        m_ActionRelease?.Invoke(item);
        m_stack.Push(item);   //把该item放入栈中
    }
}

public class TestFunc : MonoBehaviour
{
    ObjectPool<StringBuilder> sbPool = new ObjectPool<StringBuilder>(ActionGet, ActionRelease);

    private void Start()
    {
        StringBuilder sb = sbPool.Get();
        sb.Append("<color=yellow>hello, world</color>");
        Debug.Log(string.Format("GET: 总数量:{0}, activeCount: {1}, inactiveCount: {2}, after APPEND: {3}", 
                                 sbPool.totalCount, sbPool.activeCount, sbPool.inactiveCount, sb.ToString()));

        //用完该item后释放
        sbPool.Release(sb);
        Debug.Log(string.Format("RELEASE: 总数量: {0}, activeCount: {1}, inactiveCount: {2}",
                                      sbPool.totalCount, sbPool.activeCount, sbPool.inactiveCount));
    }

    //由于需要将该方法直接赋值给ObjectPool的构造方法,因此需要声明为static
    private static void ActionGet(StringBuilder sb)
    {
        Debug.Log(string.Format("成功获取到一个item:{0}", sb.ToString()));
    }

    private static void ActionRelease(StringBuilder sb)
    {
        Debug.Log(string.Format("释放一个item 前: {0}", sb.ToString()));
        sb.Length = 0;   //对该item对象做处理,如果是自定义类型,则可以自定义某些执行方法
    }
}

operation result:

Notice:

1. The object pool template is more like a data collection, and the effect is different from the singleton template, so it does not need to be inherited

2. When directly assigning a value to the constructor from the outside, the parameter or method needs to be statically modified:

 

PS:

1. The role of readonly——A variable modified with readonly can only be assigned a value at the time of declaration and in the constructor, and its value cannot be changed in other methods. And in view of the characteristics of the reference type, the variable modified by readonly is only the address of the reference type variable, so the address of the arr cannot be changed, that is, a new reference type variable cannot be used to assign arr, but the content of the reference type object can be changed itself , that is, after "readonly int[] arr = new int[10]", "arr = new int[6]" cannot be executed in any method except the construction method, but "arr[1] can be executed in any method = 100". The value type variable is different, such as "readonly int num = 100", except for the construction method, the value of num cannot be changed again in any other place :

class Test
{
    readonly int num = 100;
    readonly int[] arr = new int[10];

    //readonly修饰的变量只可以在声明时和构造函数中才可以改变其值,无法在其他方法中被赋值
    public Test(int _num, int[] _arr)   //构造方法声明为public主要是为了可以在外部被调用
    {
        _num = 101;
        _arr = new int[12];
    }

    void Method()
    {
        arr[1] = 200;   //不改变arr所指向的引用类型对象地址,改变的是该引用类型对象本身

        //以下会编译报错
        arr = new int[6]; //此时改变的是arr的引用类型对象,因此会编译报错
        num = 200;  //值类型对象,无法改变其值,因此编译报错
    }
}

2. In the use of Stack, stack.peek () returns the top element of the stack, but does not pop the top element of the stack , while stack.pop () returns the top element of the stack and pops the top element of the stack .

3. A class containing a generic type T does not contain a T parameter in its constructor:

4. To clear the StringBuilder object, you can use StringBuilder.Clear or directly StringBuilder.Length = 0 :

StringBuilder sb = new StringBuilder("abc");

void Start()
{
	Debug.Log(string.Format("before: {0}", sb.ToString()));
	sb.Clear();
	Debug.Log(string.Format("after CLEAR: {0}", sb.ToString()));
	sb.Append("def");
	Debug.Log(string.Format("after APPEND: {0}", sb.ToString()));
	sb.Length = 0;
	Debug.Log(string.Format("after SB.LENGTH = 0: {0}", sb.ToString()));
}

operation result:

 

3. Double-click to exit function: double-click to return in a short period of time on the Android phone to exit the current program

#region 用户连续点两个返回键则退出
float exitTimeCountdown = 1f;   //用于判断用户短时间内连续按两次返回键后退出程序
int exitBtnClickCount = 0;     //返回按钮点击的次数
void QuitGame()
{
	if (exitBtnClickCount > 0)
	{
		exitTimeCountdown -= Time.deltaTime;
		if (exitTimeCountdown < 0)      //在倒计时内没有连续点击,因此重置
		{
			exitBtnClickCount = 0;
			exitTimeCountdown = 1;
		}
	}

	if (Input.GetKeyDown(KeyCode.Escape))
	{
		exitBtnClickCount += 1;
		if (exitBtnClickCount >= 2)
			Application.Quit();
	}
}

private void Update()
{
	QuitGame();
}
#endregion

4. Swipe to turn pages, double-tap the screen, swipe to rotate an object, two fingers to zoom in or out the screen: 

Swipe to turn pages: 

#region 滑动翻页
Touch oneFingerTouch;
Vector2 startPos, endPos;
Vector2 direction;
void SlideDetect()
{
	//这里为了避免和放大缩小屏幕或其他touch功能有关联,只单独检测一个touch时的情况
	if (Input.touchCount == 1)
	{
		oneFingerTouch = Input.GetTouch(0);
		//通过Touch的各个阶段来获取滑动状态
		if (oneFingerTouch.phase == TouchPhase.Began)
			startPos = oneFingerTouch.position;
		else if (oneFingerTouch.phase == TouchPhase.Ended)
		{
			endPos = oneFingerTouch.position;
			direction = (endPos - startPos).normalized;

			//开始判定方向:根据 y = x 和 y = -x 两条线来判定上下左右方向。(根号2)/2 = 0.7左右
			if (direction.y > 0 && Mathf.Abs(direction.x) <= 0.7)
				GameMgr.GetInstance().msg = string.Format("向上滑动: {0}", direction);
			else if (direction.y < 0 && Mathf.Abs(direction.x) <= 0.7)
				GameMgr.GetInstance().msg = string.Format("向下滑动:{0}", direction);
			else if (direction.x < 0 && Mathf.Abs(direction.y) < 0.7)
				GameMgr.GetInstance().msg = string.Format("向左滑动:{0}", direction);
			else if (direction.x > 0 && Mathf.Abs(direction.y) < 0.7)
				GameMgr.GetInstance().msg = string.Format("向右滑动: {0}", direction);
		}
	}
}
#endregion

Double-tap the screen: For devices that support "Input.touchPressureSupported", you can use "touch.pressure" to get the size of the pressing force. For devices that do not support touchPressureSupported, the return value of touch.pressure is always 1

#region 双击屏幕
void DoubleTapScreen()
{
	if (Input.touchCount == 1)
	{
		Touch t = Input.GetTouch(0);
		GameMgr.GetInstance().msg = string.Format("PressureSupported: {0}, Pressure value is: {1}, TapCount is: {2}", Input.touchPressureSupported, t.pressure, t.tapCount);
		if (t.tapCount == 2)
			GameMgr.GetInstance().msg = "双击屏幕了";
	}
}
#endregion

Rotate a GameObject while sliding:

#region 滑动时旋转某个GameObject
void RotateBySlide()
{
	if (Input.touchCount == 1)
	{
		GameObject obj = GameObject.Find("Cube");
		Touch m_touch = Input.GetTouch(0);
		//绕Y轴旋转
		obj.transform.Rotate(Vector3.up, m_touch.deltaPosition.x * Time.deltaTime, Space.Self);
	}
}
#endregion

Pinch to zoom in or out the entire view:

#region 双指放大或缩小屏幕
//这里的放大或缩小仅仅只是针对3D世界的场景,拉近或拉远Camera,对于UI界面则没有效果
//改变UI界面的大小,则需要改变CanvasScaler中的“scale factor”
Touch touch1, touch2;
float previousDis = 0;    //之前的距离,初始值为0,用于判定滑动的初始状态。由于两个touch之间的dis不可能为0,所以可以由此来判定是否为初始状态
float currentDis, deltaDis;
void AdjustFovSize()
{
	if (Input.touchCount == 2)
	{
		touch1 = Input.GetTouch(0);
		touch2 = Input.GetTouch(1);
		if (touch1.phase == TouchPhase.Moved || touch2.phase == TouchPhase.Moved)
		{
			currentDis = (touch1.position - touch2.position).magnitude;

			if (previousDis == 0)     //初始状态,此时记录下两者之间的距离
				previousDis = currentDis;
			else
			{
				deltaDis = currentDis - previousDis;

				//3D世界中camera拉近:鉴于FOV的特点,当数值减少时是拉近camera的效果,故使用“-”
				Camera.main.fieldOfView -= deltaDis * Time.deltaTime;

				//UGUI界面的放大 —— 如果确定需要放大UGUI,可以将“Canvas Scaler”单独提取出来,避免每次在update中监测
				GameObject obj = GameObject.Find("Canvas");
				CanvasScaler scaler = obj.GetComponent<CanvasScaler>();
				scaler.scaleFactor += deltaDis * Time.deltaTime * 0.005f;  
				//放大所有UI元素,“0.005f”是为了防止变化过大而加入的数值,可以根据效果来自由设定。可以尽量和FOV改变的效果近似
			}
		}
	}
	else
	{
		previousDis = 0;    //重置为初始状态
	}
}
#endregion

5. Flexible use of attributes:

In some cases, it may be necessary to display all important msgs of the application to facilitate debugging. Here you can use attributes to achieve good results:

private StringBuilder sb = new StringBuilder();
public string msg
{
	get
	{
		return string.Format("<size=20><color=yellow>{0}</color></size>", sb.ToString());
	}
	set
	{
		//最新的消息显示在最上面
		if (!string.IsNullOrEmpty(sb.ToString()))
			sb.Insert(0, "\n");            //加入换行,方便显示
		sb.Insert(0, value);
	}
}


...........

private void OnGUI()
{
	if (GUI.Button(new Rect(50, 50, 200, 100), "<size=20><color=yellow>Clear</color></size>"))
		sb.Clear();
	GUI.Label(new Rect(50, 200, Screen.width - 400, Screen.height - 200), msg);
}

Use StringBuilder.Insert to always display the latest msg at the top. When there are a lot of msg in a short period of time, you can see the effect of the message scrolling quickly.

PS: The role of private modifiers in get and set accessors in attributes:

When the private modifier is added before the get or set accessor, it means that the accessor can only be called in the class or structure that declares the property, and the accessor cannot be called externally:

class A
{
    public int num
    {
        get;
        private set;
    }

    public void Method()
    {
        Debug.Log(string.Format("First - num: {0}", num));
        ++num;
        Debug.Log(string.Format("Two - num: {0}", num));
    }
}
public class TestFunc : MonoBehaviour
{
    void Start()
    {
        A a = new A();
        //get访问器
        Debug.Log(string.Format("Third - num: {0}", a.num));

        //set访问器
        a.Method();     //在类A内部是可以正常访问

        a.num = 5;   //编译报错,在外部无法访问set
    }
}

The execution result after commenting out "a.num = 5":

Note: When the get is set to private in the attribute, it is usually to prohibit access to some sensitive data ; and when the set is set to private, it can achieve the effect of protecting data security. private means that the accessor can only be accessed inside the class or struct where the attribute is declared

 

Guess you like

Origin blog.csdn.net/m0_47975736/article/details/123605383