Unity中常用的单例模式、对象池的脚本模板,连按退出和滑动翻页或放大缩小的功能实现,以及属性在代码中的灵活使用

1.单例模式的脚本模板:

Unity中针对一些常用的manager可以使用单例模式,用于统一的功能管理:

//普通单例,不需要继承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>
{
    ........
    ........
}

但是对于Unity中某些需要作为Component挂载在GameObject上的单例类则需要做如下设置:

//该类型的单例需要继承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并不是MonoBehavior中的方法,为什么一定要继承MonoBehavior?

解析:Unity引擎中有“Message System”,MonoBehavior用于对Unity的消息系统进行监听当某些指定的事件触发时,所有继承了MonoBehavior并挂载在GameObject上的Component都会对事件做出响应,根据事件类型的不同调用该类中不同的方法,如:Awake, Update, OnApplicationQuit/Pause, OnCollisionEnter, OnMouseDrag, OnDestroy, OnDisable等,具体可查询“Messages”模块:Unity - Scripting API: MonoBehaviour

如果脚本A中没有“碰撞、拖动、程序退出”等事件B所对应的被调用方法如OnCollisionEnter, OnMouseDrag, OnApplicationQuit等,则该脚本A不会对事件B做出响应。

这些方法如Awake, Start, Update, OnCollisionEnter等并不是MonoBehavior中自带的方法,在脚本中声明该方法时,方法前并没有“override”修饰符,并且还有“private”关键字:

参考链接: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.要把脚本挂载在GameObject上,则该脚本必须继承自MonoBehavior,而不是Component

2.对象池模板:

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对象做处理,如果是自定义类型,则可以自定义某些执行方法
    }
}

运行结果:

注意:

1.对象池模板更像是一种数据集合,与单例模板效果是不一样的,所以并不需要继承

2.从外部直接给构造方法赋值时,需要该参数或者方法为static修饰:

PS:

1.readonly的作用 —— 使用readonly修饰的变量只能在声明时和构造函数中被赋值,无法在其他方法中改变其值。并且鉴于引用类型的特性,readonly修饰的变量只是该引用类型变量的地址,所以无法改变该arr的地址指向,即无法用一个新的引用类型变量来赋值arr,但是可以改变该引用类型对象的内容本身,即“readonly int[] arr = new int[10]”后,除了构造方法外无法在任何方法内执行“arr = new int[6]”,但可以在任何方法中执行“arr[1] = 100”。而值类型变量则不同,如“readonly int num = 100”后,除了构造方法外,无法在任何其他地方再次改变num的值

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.在Stack的使用中,stack.peek()是返回栈顶的元素,但不弹出该栈顶元素,而stack.pop()是返回该栈顶元素,并将该栈顶元素出栈

3.包含泛型T的类,其构造函数中不包含T参数:

4.将StringBuilder对象清空可使用StringBuilder.Clear或直接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()));
}

运行结果:

3.连按退出功能:在安卓手机上短时间内连按两次返回则退出当前程序

#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.滑动翻页,双击屏幕,滑动旋转某个物体,双指放大或缩小屏幕: 

滑动翻页: 

#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

双击屏幕:对于支持“Input.touchPressureSupported”的设备,可以使用“touch.pressure”获取按压力的大小。对于不支持touchPressureSupported的设备,则touch.pressure返回值始终为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

滑动时旋转某个GameObject:

#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

双指放大或缩小整个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.属性的灵活使用:

某些情况下可能需要将应用的重要msg都显示出来,方便调试,这里可以使用属性可以达到很好的效果:

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

使用StringBuilder.Insert始终将最新的msg在最上面显示,当短时间内msg很多时则可以看到消息很快滚动的效果。

PS:属性中get、set访问器使用private修饰符的作用:

当在get或set访问器前添加private修饰符时表示该访问器只可以在声明该属性的类或者结构体中被调用,外部无法调用该访问器:

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

注释掉“a.num = 5”后执行结果:

注意:当属性中设定get为private时,通常是为了禁止某些敏感数据的访问权限而设定set为private时,则可以达到保护数据安全的效果。private是现在该访问器只能在声明属性的类或者struct的内部才可被访问

猜你喜欢

转载自blog.csdn.net/m0_47975736/article/details/123605383