Unity游戏基本框架
Posted TongOuO
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity游戏基本框架相关的知识,希望对你有一定的参考价值。
个人总结笔记,参考自B站各教程,希望对他人也有所帮助,对我自己也方便复习。
感谢唐老狮的教学
目录
文件夹管理
-
Resources文件夹加载资源(其中所有东西最终都会被打包不管有没有用到)
-
Scripts文件夹放置相关代码
- ProjectBase工程基础文件
- Base单例模式基类
- Pool文件夹
- ProjectBase工程基础文件
-
Scenes场景资源放置一些保存好的场景
-
ArtRes文件夹直接将外部资源导入于此(减少游戏包大小)
单例模式基类
作用:减少单例模式重复代码的书写
作为管理者不继承Monobehaviour
单例模式介绍
一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
使用单例模式可以减少资源消耗
饿汉式:唯一实例在类加载时立即进行实例化
懒汉式:在类加载时不进行实例化,在第一次使用时进行实例化
双重检查锁解决线程问题
BV1af4y1y7sS
Unity中的单例模式
一个静态成员变量类型是自身
公共的静态成员方法/属性
Unity小游戏中一般不考虑双锁线程问题所以只需要简单写懒汉式即可
基础代码
public class GameManager
private static GameManager instance;
public static GameManager Getinstance()
if(instance == null)
instance = new GameManager();
return instance;
升级版2.0加入泛型
public class BaseManager<T> where T:new()
private static T instance;
public static T Getinstance()
if(instance == null)
instance = new T();
return instance;
它的子类管理类只需要继承自他然后把自身的类型传进去即可
public class GameManager :BaseManager<GameManager>
C#中泛型的知识
BV1A4411F7fj
泛型的作用
- 跨类型的可复用的代码:继承和泛型
- 继承->基类
- 泛型->带有类型占位符的模板
Generic types泛型类型
开放类型和封闭类型
泛型方法
泛型方法中需要引用泛型参数
补充:ref和out基本一样,即代替了c中指针的部分作用,区别是即在使用ref,和不使用修饰符的时候,必须要传递一个有值的参数。ref和out几乎就只有一个区别,那就是out可以使用未赋值的变量。
原因是out应该是在方法内部做了分配地址的操作,然后把地址赋给外部的变量。但是ref的话是直接传递外部地址进方法。
声明泛型类型
- 可以有多个泛型参数
Typeof可以实现将未绑定的泛型类型存在
default方法可以获得泛型类型的默认值
泛型的约束
继承MonoBehaviour的单例基类模式
Unity中不适用new的方法创建实例(继承了mono的脚本)
只能通过拖动到对象上或者通过加脚本api Addcomponent去加脚本
U3D内部帮我们实现他
引用:最后总结一下Awake和Start的异同点:
相同点:
1)两者都是对象初始化时调用的,都在Update之前,场景中的对象都生成后才会调用Awake,Awake调用完才会调用Start,所有Start调用完才会开始Update。
2)两者在对象生命周期内都只会被调用一次,即初始化时被调用,之后即使是在被重新激活之后也不会再次被调用。
不同点:
1)Awake函数在对象初始化之后立刻就会调用,换句话说,对象初始化之后第一调用的函数就是Awake;而Start是在对象初始化后,第一次Update之前调用的,
在 Start中进行初始化不是很安全,因为它可能被其他自定义的函数抢先。
2)Awake不管脚本是否enabled都会被调用;而Start如果对象被SetAcive(false)或者enabled= false了是不会被 调用的。
3)如果对象(GameObject)本身没激活,那么Awake,Start都不会调用。
单例模式mono基础代码
public class NewBehaviourScript : MonoBehaviour
private static NewBehaviourScript instance;
public static NewBehaviourScript GetInstance()
return instance;
void Awake()
instance = this;
采用instance方法而不用addcomponent方法是因为挂载后再instance=this的方法可以解决重复问题用新的顶掉旧的,而addcomponent多挂一遍就会多加一个不太行。
单例模式mono基类代码
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
private static T instance;
public static T GetInstance()
return instance;
protected virtual void Awake()
instance = this as T;
采用virtual虚函数子类保证子类可以对Awake进行重写
子类再用protected override进行重写Awake
继承了Monobehaviour的单例模式对象需要我们自己保证其唯一性
升级版AutoSingletonMono继承这种单例模式的不需要直接去拖直接Getinstance就好了
public class SingletonAutoMono<T> : MonoBehaviour where T: MonoBehaviour
private static T instance;
public static T GetInstance()
if(instance == null)
GameObject obj = new GameObject();
obj.name = typeof(T).ToString();
DontDestroyOnLoad(obj);//保证物体过场景不被销毁
instance = obj.AddComponent<T>();
return instance;
缓存池模块
缓存池模块本质上是一个抽屉来存储暂时没有用的东西供给以后用
原理是减少GC次数减少卡顿短期增加内存
从而实现手动GC而减少卡顿
常见数据结构的使用情景
Array
需要处理的元素数量确定并且需要使用下标时可以考虑,不过建议使用List
ArrayList
不推荐使用,建议用List
List泛型List
需要处理的元素数量不确定时 通常建议使用
LinkedList
链表适合元素数量不固定,需要经常增减节点的情况,2端都可以增减
Queue
先进先出的情况
Stack
后进先出的情况
Dictionary<K,T>
需要键值对,快速操作
PoolMgr
用dictionary键值对来对应存储相应的种类名字与list对应
用list而不用数组方便管理和动态存储
定义字典(类名-list)
public Dictionary<string,List> poolDic = new Dictionary<string, List>();
定义存入和取出函数
取出函数写法:
判断有无对应name下的缓存模块
如果没有直接进行实例化将name作为路径传入
如果有直接取出第0个obj并且移除位于list中的obj
并且进行object激活
存入函数写法
存入也就是销毁物体
如果缓存池中无name对应list创建name和list,如果已经有list就加上这个obj
Invoke复习
Invoke(“SendMsg”, 5); 它的意思是:5 秒之后调用 SendMsg() 方法
Invoke(); 不能接受含有 参数的方法;
Invoke() 也支持重复调用:InvokeRepeating(“SendMsg”, 2 , 3);
CancelInvoke();取消调用
注意:常用OnEnable函数(当对象激活时会进入声明周期函数)代替Start函数因为反复调用的时候Start函数只会进行第一次
public class PoolMgr : BaseManager<PoolMgr>
public Dictionary<string,List<GameObject>> poolDic = new Dictionary<string, List<GameObject>>();
public GameObject Getobj(string name)
GameObject obj = null;
if ( poolDic.ContainsKey(name)&& poolDic[name].Count >0)
obj =poolDic[name][0];
poolDic[name].RemoveAt(0);
else
obj= GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name;//很关键:把对象名改成池子名字一样防止因clone导致的错误
obj.SetActive(true);
return obj;
public void PushObj(string name,GameObject obj )
obj.SetActive(false);
if(poolDic.ContainsKey(name))
poolDic[name].Add(obj);
else
poolDic.Add(name,new List<GameObject>()obj);
对缓存池的优化
增加一个Pool父物体,将缓存池中的子物体存储于其中,在get的时候解除父子关系调出,在false的时候进入父子关系
在切换场景的时候原先的物体已经清除了但原先内存上的关联关系仍然没有被清除所以会产生错误,解决的方法是新增clear方法。
public void clear()
poolDic.Clear();
poolObj= null;
再给Pool物体区分的更细节(这里先省略一下,以后再补,有点难以理解而且我暂时没用到不太需要)
public class PoolData
public GameObject fatherObj;
public List<GameObject> poolList;
public PoolData(GameObject obj,GameObject poolObj)
fatherObj = new GameObject(obj.name);
fatherObj.transform.parent=poolObj.transform;
poolList = new List<GameObject>()obj ;
缓存池模块结合资源加载模块
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 抽屉数据 池子中的一列容器
/// </summary>
public class PoolData
//抽屉中 对象挂载的父节点
public GameObject fatherObj;
//对象的容器
public List<GameObject> poolList;
public PoolData(GameObject obj, GameObject poolObj)
//给我们的抽屉 创建一个父对象 并且把他作为我们pool(衣柜)对象的子物体
fatherObj = new GameObject(obj.name);
fatherObj.transform.parent = poolObj.transform;
poolList = new List<GameObject>() ;
PushObj(obj);
/// <summary>
/// 往抽屉里面 压都东西
/// </summary>
/// <param name="obj"></param>
public void PushObj(GameObject obj)
//失活 让其隐藏
obj.SetActive(false);
//存起来
poolList.Add(obj);
//设置父对象
obj.transform.parent = fatherObj.transform;
/// <summary>
/// 从抽屉里面 取东西
/// </summary>
/// <returns></returns>
public GameObject GetObj()
GameObject obj = null;
//取出第一个
obj = poolList[0];
poolList.RemoveAt(0);
//激活 让其显示
obj.SetActive(true);
//断开了父子关系
obj.transform.parent = null;
return obj;
/// <summary>
/// 缓存池模块
/// 1.Dictionary List
/// 2.GameObject 和 Resources 两个公共类中的 API
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
//缓存池容器 (衣柜)
public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
private GameObject poolObj;
/// <summary>
/// 往外拿东西
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public void GetObj(string name, UnityAction<GameObject> callBack)
//有抽屉 并且抽屉里有东西
if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
callBack(poolDic[name].GetObj());
else
//通过异步加载资源 创建对象给外部用
ResMgr.GetInstance().LoadAsync<GameObject>(name, (o) =>
o.name = name;
callBack(o);
);
//obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
//把对象名字改的和池子名字一样
//obj.name = name;
/// <summary>
/// 换暂时不用的东西给我
/// </summary>
public void PushObj(string name, GameObject obj)
if (poolObj == null)
poolObj = new GameObject("Pool");
//里面有抽屉
if (poolDic.ContainsKey(name))
poolDic[name].PushObj(obj);
//里面没有抽屉
else
poolDic.Add(name, new PoolData(obj, poolObj));
/// <summary>
/// 清空缓存池的方法
/// 主要用在 场景切换时
/// </summary>
public void Clear()
poolDic.Clear();
poolObj = null;
事件中心模块
- 制作成就系统
- 任务记录
- 达成某种条件等等
减少代码量,减少复杂性,降低程序的耦合度
核心思路:设置事件中心将事件加进去事件发生时通知监听者
原理:基于字典和委托
KEY–事件的名字(玩家死亡,怪物死亡)
value–监听这个事件的对应的委托函数们
因为委托可以通过+=和-=所以可以有很多委托
需要事件的监听方法:两个参数,事件名和其监听者的委托函数
事件触发函数:得到哪个函数被触发了 参数是name 找到委托函数并且触发
public class EventCenter : BaseManager<EventCenter>
private Dictionary<string,UnityAction> eventDic =new Dictionary<string, UnityAction>();
public void AddEventListener(string name,UnityAction action)
if(eventDic.ContainsKey(name))
eventDic[name]+=action;
else
eventDic.Add(name,action);
public void EventTrigger(string name)
if(eventDic.ContainsKey(name))
eventDic[name]();
事件中心会由开始到程序结束都存在
将UnityAction中传入Object参数增强事件系统的通用性
public class EventCenter : BaseManager<EventCenter>
private Dictionary<string,UnityAction<object>> eventDic =new Dictionary<string, UnityAction<object>>();
public void AddEventListener(string name,UnityAction<object> action)
if(eventDic.ContainsKey(name))
eventDic[name]+=action;
else
eventDic.Add(name,action);
public void EventTrigger(string name,object info)
if(eventDic.ContainsKey(name))
eventDic[name](info);
public void RemoveEventListener(string name,UnityAction<object> action)
if(eventDic.ContainsKey(name))
eventDic[name]-=action;
public void Clear()
eventDic.Clear();
对事件中心的优化以及避免装箱拆箱
方法是使用泛型类型,字典中存储泛型的基类接口来实现减少传入参数为object带来的装箱拆箱所造成的消耗,并且使用重载的方法处理不带有类型参数的情况
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public interface IEventInfo
public class EventInfo<T> : IEventInfo
public UnityAction<T> actions;
public EventInfo( UnityAction<T> action)
actions += action;
public class EventInfo : IEventInfo
public UnityAction actions;
public EventInfo(UnityAction action)
actions += action;
/// <summary>
/// 事件中心 单例模式对象
/// 1.Dictionary
/// 2.委托
/// 3.观察者设计模式
/// 4.泛型
/// </summary>
public class EventCenter : BaseManager<EventCenter>
//key —— 事件的名字(比如:怪物死亡,玩家死亡,通关 等等)
//value —— 对应的是 监听这个事件 对应的委托函数们
private Dictionary<string, IEventInfo> eventDic = new Dictionary<string, IEventInfo>();
/// <summary>
/// 添加事件监听
/// </summary>
/// <param name="name">事件的名字</param>
/// <param name="action">准备用来处理事件 的委托函数</param>
public void AddEventListener<T>(string name, UnityAction<T> action)
//有没有对应的事件监听
//有的情况
if( eventDic.ContainsKey(name) )
(eventDic[name] as EventInfo<T>).actions += action;
//没有的情况
else
eventDic.Add(name, new EventInfo<T>( action ));
/// <summary>
/// 监听不需要参数传递的事件
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
public void AddEventListener(string name, UnityAction action)
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
(eventDic[name] as EventInfo).actions += action;
//没有的情况
else
eventDic.Add(name, new EventInfo(action));
/// <summary>
/// 移除对应的事件监听
/// </summary>
/// <param name="name">事件的名字</param>
/// <param name="action">对应之前添加的委托函数</param>
public void RemoveEventListener<T>(string name, UnityAction<T> action)
if (eventDic.ContainsKey(name))
(eventDic[name] as EventInfo<T>).actions -= action;
/// <summary>
/// 移除不需要参数的事件
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
public void RemoveEventListener(string name, UnityAction action)
if (eventDic.ContainsKey(name))
(eventDic[name] as EventInfo).actions -= action;
/// <summary>
/// 事件触发
/// </summary>
/// <param name="name">哪一个名字的事件触发了</param>
public void EventTrigger<T>(string name, T info)
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
//eventDic[name]();
if((eventDic[name] as EventInfo<T>).actions != null)
(eventDic[name] as EventInfo<T>).actions.Invoke(info);
//eventDic[name].Invoke(info);
/// <summary>
/// 事件触发(不需要参数的)
/// </summary>
/// <param name="name"></param>
public void EventTrigger(string name)
//有没有对应的事件监听
//有的情况
if (eventDic.ContainsKey(name))
//eventDic[name]();
if ((eventDic[name] as EventInfo).actions != null)
(eventDic[name] as EventInfo).actions.Invoke();
//eventDic[name].Invoke(info);
/// <summary>
/// 清空事件中心
/// 主要用在 场景切换时
/// </summary>
public void Clear()
eventDic.Clear();
公共Mono模块
作用:让没有继承mono的类可以开启协程进行update真更新
Mono的管理者
类中的构造函数在被new的时候可以进行执行
public class MonoController : MonoBehaviour
private event UnityAction updateEvent;
void Start()
DontDestroyOnLoad(this.gameObject);
void Update()
if(updateEvent != null)
updateEvent();
public void AddUpdateListener(UnityAction fun)
updateEvent+=fun;
public void RemoveUpdateListener(UnityAction fun)
updateEvent-=fun;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 1.可以提供给外部添加帧更新事件的方法
/// 2.可以提供给外部添加 协程的方法
/// </summary>
public class MonoMgr : BaseManager<MonoMgr>
private MonoController controller;
public MonoMgr()
//保证了MonoController对象的唯一性
GameObject obj = new GameObject("MonoController");
controller = obj.AddComponent<MonoController>();
/// <summary>
/// 给外部提供的 添加帧更新事件的函数
/// </summary>
/// <param name="fun"></param>
public void AddUpdateListener(UnityAction fun)
controller.AddUpdateListener(fun);
/// <summary>
/// 提供给外部 用于移除帧更新事件函数
/// </summary>
/// <param name="fun"></param>
public void RemoveUpdateListener(UnityAction fun)
controller.RemoveUpdateListener(fun);
public Coroutine StartCoroutine(IEnumerator routine)
return controller.StartCoroutine(routine);
public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value)
return controller.StartCoroutine(methodName, value);
public Coroutine StartCoroutine(string methodName)
return controller.StartCoroutine(methodName);
可以提供给外部没有继承自Mono方法真更新的方法和协程的方法
new一个t再调用monoMgr.getinstance.addupdatelistener(t.事件名)就可以实现真更新
场景切换模块
目的是当从场景A切换到场景B的时候需要do sth动态的创建一些玩家物件
同步的时候会出现卡顿的情况
u3d异步加载会和协程配合使用
需要有ienumerator
编写一个接口调用unity自带的u3dasync加载
用协程来配合异步加载
ao.progress可以得到场景加载的进度
再结合事件中心分发事件更新加载条
执行完毕之后调用传入的fun方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
/// <summary>
/// 场景切换模块
/// 知识点
/// 1.场景异步加载
/// 2.协程
/// 3.委托
/// </summary>
public class ScenesMgr : BaseManager<ScenesMgr>
/// <summary>
/// 切换场景 同步
/// </summary>
/// <param name="name"></param>
public void LoadScene(string name, UnityAction fun)
//场景同步加载
SceneManager.LoadScene(name);
//加载完成过后 才会去执行fun
fun();
/// <summary>
/// 提供给外部的 异步加载的接口方法
/// </summary>
/// <param name="name"></param>
/// <param name="fun"></param>
public void LoadSceneAsyn(string name, UnityAction fun)
MonoMgr.GetInstance().StartCoroutine(ReallyLoadSceneAsyn(name, fun));
/// <summary>
/// 协程异步加载场景
/// </summary>
/// <param name="name"></param>
/// <param name="fun"></param>
/// <returns></returns>
private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction fun)
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
//可以得到场景加载的一个进度
while(!ao.isDone)
//事件中心 向外分发 进度情况 外面想用就用
EventCenter.GetInstance().EventTrigger("进度条更新", ao.progress);
//这里面去更新进度条
yield return ao.progress;
//加载完成过后 才会去执行fun
fun();
Unity协程
BV1N4411B7i3
协程和C#的迭代器有关IEnumerator
协程的开始在Start这种一次性执行的函数之中调用不可以在Update中调用StartCoroutine
yield return null实现以下的代码都在下一帧中进行运行。
协程在update和lateupdate之间执行的
yell return 数字表示协程后面的所有代码在下一帧进行执行
资源加载模块
同步加载资源和异步加载资源
泛型作用:加载后判断是否为Gameobject直接进行实例化否则就返回
开协程通常都是一个方法是调用真正的协程函数
里式转换原则用基类存子类可以将子类转换为基类
异步具体什么时间调用时Unity内部的协程调度器控制的,它会根据你yield return返回的内容自己去判断到底何时继续执行后面的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 资源加载模块
/// 1.异步加载
/// 2.委托和 lambda表达式
/// 3.协程
/// 4.泛型
/// </summary>
public class ResMgr : BaseManager<ResMgr>
//同步加载资源
public T Load<T>(string name) where T:Object
T res = Resources.Load<T>(name);
//如果对象是一个GameObject类型的 我把他实例化后 再返回出去 外部 直接使用即可
if (res is GameObject)
return GameObject.Instantiate(res);
else//TextAsset AudioClip
return res;
//异步加载资源
public void LoadAsync<T>(string name, UnityAction<T> callback) where T:Object
//开启异步加载的协程
MonoMgr.GetInstance().StartCoroutine(ReallyLoadAsync(name, callback));
//真正的协同程序函数 用于 开启异步加载对应的资源
private IEnumerator ReallyLoadAsync<T>(string name, UnityAction<T> callback) where T : Object
ResourceRequest r = Resources.LoadAsync<T>(name);
yield return r;
if (r.asset is GameObject)
callback(GameObject.Instantiate(r.asset) as T);
else
callback(r.asset as T);
unity委托
BV1Bk4y1B7DN
委托:存储封装几个方法
委托的构造
输入输出模块
用统一的输出输出Mgr检测输入输出来
一旦有某个输入就用分发事件分发到事件中心告诉他触发了
使用的时候只需要在start里面将键盘输入设为true并且添加监听方法就可以了
可以综合Switch语句使用来判断具体按了哪个键位
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 1.Input类
/// 2.事件中心模块
/// 3.公共Mono模块的使用
/// </summary>
public class InputMgr : BaseManager<InputMgr>
private bool isStart = false;
/// <summary>
/// 构造函数中 添加Updata监听
/// </summary>
public InputMgr()
MonoMgr.GetInstance().AddUpdateListener(MyUpdate);
/// <summary>
/// 是否开启或关闭 我的输入检测
/// </summary>
public void StartOrEndCheck(bool isOpen)
isStart = isOpen;
/// <summary>
/// 用来检测按键抬起按下 分发事件的
/// </summary>
/// <param name="key"></param>
private void CheckKeyCode(KeyCode key)
//事件中心模块 分发按下抬起事件
if (Input.GetKeyDown(key))
EventCenter.GetInstance().EventTrigger("某键按下", key);
//事件中心模块 分发按下抬起事件
if (Input.GetKeyUp(key))
EventCenter.GetInstance().EventTrigger("某键抬起", key);
private void MyUpdate()
//没有开启输入检测 就不去检测 直接return
if (!isStart)
return;
CheckKeyCode(KeyCode.W);
CheckKeyCode(KeyCode.S);
CheckKeyCode(KeyCode.A);
CheckKeyCode(KeyCode.D);
关于引用类型和值类型 装箱拆箱的复习
BV1oq4y1H7wz
值类型向引用类型转化称为装箱
引用类型向值类型转化称为拆箱
音效管理模块
区分背景音乐和音效
要有 播放音效 停止音效 播放背景音乐 停止背景音乐 四个方法
大部分的游戏并非使用的3D音乐而是2D音乐即不随着远近改变而改变
声明唯一的背景音乐 创建Gameobject直接挂上
暂停音乐
播放音乐
改变音量大小
播放音效的方法
播放音效创建Gameobject也挂上,区别是需要委托函数并且主要内容需要写在异步加载后
可以结合缓存池
需要检测自己是否播放完播放完就把自己移除掉防止内存占用过高导致崩溃
关键属性:唯一的背景音乐组件,音乐大小,音效依附对象,音效列表,音效大小
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class MusicMgr : BaseManager<MusicMgr>
//唯一的背景音乐组件
private Audiosource bkMusic = null;
//音乐大小
private float bkValue = 1;
//音效依附对象
private GameObject soundObj = null;
//音效列表
private List<AudioSource> soundList = new List<AudioSource>();
//音效大小
private float soundValue = 1;
public MusicMgr()
MonoMgr.GetInstance().AddUpdateListener(Update);
private void Update()
for( int i = soundList.Count - 1; i >=0; --i )
if(!soundList[i].isPlaying)
GameObject.Destroy(soundList[i]);
soundList.RemoveAt(i);
/// <summary>
/// 播放背景音乐
/// </summary>
/// <param name="name"></param>
public void PlayBkMusic(string name)
if(bkMusic == null)
GameObject obj = new GameObject();
obj.name = "BkMusic";
bkMusic = obj.AddComponent<AudioSource>();
//异步加载背景音乐 加载完成后 播放
ResMgr.GetInstance().LoadAsync<AudioClip>("Music/BK/" + name, (clip) =>
bkMusic.clip = clip;
bkMusic.loop = true;
bkMusic.volume = bkValue;
bkMusic.Play();
);
/// <summary>
/// 暂停背景音乐
/// </summary>
public void PauseBKMusic()
if (bkMusic == null)
return;
bkMusic.Pause();
/// <summary>
/// 停止背景音乐
/// </summary>
public void StopBKMusic()
if (bkMusic == null)
return;
bkMusic.Stop();
/// <summary>
/// 改变背景音乐 音量大小
/// </summary>
/// <param name="v"></param>
public void ChangeBKValue(float v)
bkValue = v;
if (bkMusic == null)
return;
bkMusic.volume = bkValue;
/// <summary>
/// 播放音效
/// </summary>
public void PlaySound(string name, bool isLoop, UnityAction<AudioSource> callBack = null)
if(soundObj == null)
soundObj = new GameObject();
soundObj.name = "Sound";
//当音效资源异步加载结束后 再添加一个音效
ResMgr.GetInstance().LoadAsync<AudioClip>("Music/Sound/" + name, (clip) =>
AudioSource source = soundObj.AddComponent<AudioSource>();
source.clip = clip;
source.loop = isLoop;
source.volume = soundValue;
source.Play();
soundList.Add(source);
if(callBack != null)
callBack(source);
);
/// <summary>
/// 改变音效声音大小
/// </summary>
/// <param name="value"></param>
public void ChangeSoundValue( float value )
soundValue = value;
for (int i = 0; i < soundList.Count; ++i)
soundList[i].volume = value;
/// <summary>
/// 停止音效
/// </summary>
public void StopSound(AudioSource source)
if( soundList.Contains(source) )
soundList.Remove(source);
source.Stop();
GameObject.Destroy(source);
UGUI学习
BV14v411h7UP
三种GUI的区别
OnGUI手机移动端空间用来调试
文本
IMAGE
Raw image
UGUI补充
BV14v411h7UP
Button组件可以通过transition改变各种操作时候组件本身的响应
Navigation导航可以用来切换按钮
Toggle勾选开关
ToggleGroup实现单选
可以取slider的value值从而来控制volume
同时可以更改背景和fill区域来实现血条
滚动视图用来保证一个调整x,y的组件
可以通过滚动视图完成背包效果
DropDown下拉选框可以加图片
UI模块
总结共性通用模块
面板UI基类BasePanel
目的要找到自己面板下的控件对象并且提供隐藏显示方法
找到子对象的控件用泛型存入字典
编写得到对应名字的控件方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 面板基类
/// 帮助我门通过代码快速的找到所有的子控件
/// 方便我们在子类中处理逻辑
/// 节约找控件的工作量
/// </summary>
public class BasePanel : MonoBehaviour
//通过里式转换原则 来存储所有的控件
private Dictionary<string, List<UIBehaviour>> controlDic = new Dictionary<string, List<UIBehaviour>>();
// Use this for initialization
protected virtual void Awake ()
FindChildrenControl<Button>();
FindChildrenControl<Image>();
FindChildrenControl<Text>();
FindChildrenControl<Toggle>();
FindChildrenControl<Slider>();
FindChildrenControl<ScrollRect>();
FindChildrenControl<InputField>();
/// <summary>
/// 显示自己
/// </summary>
public virtual void ShowMe()
/// <summary>
/// 隐藏自己
/// </summary>
public virtual void HideMe()
protected virtual void OnClick(string btnName)
protected virtual void OnValueChanged(string toggleName, bool value)
/// <summary>
/// 得到对应名字的对应控件脚本
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="controlName"></param>
/// <returns></returns>
protected T GetControl<T>(string controlName) where T : UIBehaviour
if(controlDic.ContainsKey(controlName))
for( int i = 0; i <controlDic[controlName].Count; ++i )
if (controlDic[controlName][i] is T)
return controlDic[controlName][i] as T;
return null;
/// <summary>
/// 找到子对象的对应控件
/// </summary>
/// <typeparam name="T"></typeparam>
private void FindChildrenControl<T>() where T:UIBehaviour
T[] controls = this.GetComponentsInChildren<T>();
for (int i = 0; i < controls.Length; ++i)
string objName = controls[i].gameObject.name;
if (controlDic.ContainsKey(objName))
controlDic[objName].Add(controls[i]);
else
controlDic.Add(objName, new List<UIBehaviour>() controls[i] );
//如果是按钮控件
if(controls[i] is Button)
(controls[i] as Button).onClick.AddListener(()=>
OnClick(objName);
);
//如果是单选框或者多选框
else if(controls[i] is Toggle)
(controls[i] as Toggle).onValueChanged.AddListener((value) =>
OnValueChanged(objName, value);
);
UI管理器UIManager
显示面板\\隐藏面板
里式转换原则:基类装子类
显示方法:创建出来 位子设置对 存到对应父物体下
需要回调函数便于修改basepanel
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// UI层级
/// </summary>
public enum E_UI_Layer
Bot,
Mid,
Top,
System,
/// <summary>
/// UI管理器
/// 1.管理所有显示的面板
/// 2.提供给外部 显示和隐藏等等接口
/// </summary>
public class UIManager : BaseManager<UIManager>
public Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();
private Transform bot;
private Transform mid;
private Transform top;
private Transform system;
//记录我们UI的Canvas父对象 方便以后外部可能会使用它
public RectTransform canvas;
public UIManager()
//创建Canvas 让其过场景的时候 不被移除
GameObject obj = ResMgr.GetInstance().Load<GameObject>("UI/Canvas");
canvas = obj.transform as RectTransform;
GameObject.DontDestroyOnLoad(obj);
//找到各层
bot = canvas.Find("Bot");
mid = canvas.Find("Mid");
top = canvas.Find("Top");
system = canvas.Find("System");
//创建EventSystem 让其过场景的时候 不被移除
obj = ResMgr.GetInstance().Load<GameObject>("UI/EventSystem");
GameObject.DontDestroyOnLoad(obj);
/// <summary>
/// 通过层级枚举 得到对应层级的父对象
/// </summary>
/// <param name="layer"></param>
/// <returns></returns>
public Transform GetLayerFather(E_UI_Layer layer)
switch(layer)
case E_UI_Layer.Bot:
return this.bot;
case E_UI_Layer.Mid:
return this.mid;
case E_UI_Layer.Top:
return this.top;
case E_UI_Layer.System:
return this.system;
return null;
/// <summary>
/// 显示面板
/// </summary>
/// <typeparam name="T">面板脚本类型</typeparam>
/// <param name="panelName">面板名</param>
/// <param name="layer">显示在哪一层</param>
/// <param name="callBack">当面板预设体创建成功后 你想做的事</param>
public void ShowPanel<T>(string panelName, E_UI_Layer layer = E_UI_Layer.Mid, UnityAction<T> callBack = null) where T:BasePanel
if (panelDic.ContainsKey(panelName))
panelDic[panelName].ShowMe();
// 处理面板创建完成后的逻辑
if (callBack != null)
callBack(panelDic[panelName] as T);
//避免面板重复加载 如果存在该面板 即直接显示 调用回调函数后 直接return 不再处理后面的异步加载逻辑
return;
ResMgr.GetInstance().LoadAsync<GameObject>("UI/" + panelName, (obj) =>
//把他作为 Canvas的子对象
//并且 要设置它的相对位置
//找到父对象 你到底显示在哪一层
Transform father = bot;
switch(layer)
case E_UI_Layer.Mid:
father = mid;
break;
case E_UI_Layer.Top:
father = top;
break;
case E_UI_Layer.System:
father = system;
break;
//设置父对象 设置相对位置和大小
obj.transform.SetParent(father);
obj.transform.localPosition = Vector3.zero;
obj.transform.localScale = Vector3.one;
(obj.transform as RectTransform).offsetMax = Vector2.zero;
(obj.transform as RectTransform).offsetMin = Vector2.zero;
//得到预设体身上的面板脚本
T panel = obj.GetComponent<T>();
// 处理面板创建完成后的逻辑
if (callBack != null)
callBack(panel);
panel.ShowMe();
//把面板存起来
panelDic.Add(panelName, panel);
);
/// <summary>
/// 隐藏面板
/// </summary>
/// <param name="panelName"></param>
public void HidePanel(string panelName)
if(panelDic.ContainsKey(panelName))
panelDic[panelName].HideMe();
GameObject.Destroy(panelDic[panelName].gameObject);
panelDic.Remove(panelName);
/// <summary>
/// 得到某一个已经显示的面板 方便外部使用
/// </summary>
public T GetPanel<T>(string name) where T:BasePanel
if (panelDic.ContainsKey(name))
return panelDic[name] as T;
return null;
Unity 框架QFramework v1.0 使用指南 架构篇:05. 引入 Utility | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏
05. 引入 Utility
在这一篇,我们来支持 CounterApp 的存储功能。
其代码也非常简单,只需要修改一部分 Model 的代码即可,如下:
// 定义一个 Model 对象
public class CounterAppModel : AbstractModel
private int mCount;
public int Count
get => mCount;
set
if (mCount != value)
mCount = value;
PlayerPrefs.SetInt(nameof(Count),mCount);
protected override void OnInit()
Count = PlayerPrefs.GetInt(nameof(Count), mCount);
这样就支持了非常基本的数据存储功能。
当然还是有一些问题,如果时候未来我们需要存储的数据非常多的时候,Model 层就会充斥大量存储、加载相关的代码。
还有就是,我们以后如果不想使用 PlayperPrefs 了,想使用 EasySave 或者 SQLite 的时候,就会造成大量的修改工作量。
于是 QFramework 提供了一个 Utility 层,专门用来解决上述两个问题的,使用方法非常简单,如下:
using UnityEngine;
using UnityEngine.UI;
namespace QFramework.Example
// 1. 定义一个 Model 对象
public class CounterAppModel : AbstractModel
private int mCount;
public int Count
get => mCount;
set
if (mCount != value)
mCount = value;
PlayerPrefs.SetInt(nameof(Count),mCount);
protected override void OnInit()
var storage = this.GetUtility<Storage>();
Count = storage.LoadInt(nameof(Count));
// 可以通过 CounterApp.Interface 监听数据变更事件
CounterApp.Interface.RegisterEvent<CountChangeEvent>(e =>
this.GetUtility<Storage>().SaveInt(nameof(Count), Count);
);
// 定义 utility 层
public class Storage : IUtility
public void SaveInt(string key, int value)
PlayerPrefs.SetInt(key,value);
public int LoadInt(string key, int defaultValue = 0)
return PlayerPrefs.GetInt(key, defaultValue);
// 2.定义一个架构(提供 MVC、分层、模块管理等)
public class CounterApp : Architecture<CounterApp>
protected override void Init()
// 注册 Model
this.RegisterModel(new CounterAppModel());
// 注册存储工具的对象
this.RegisterUtility(new Storage());
// 定义数据变更事件
public struct CountChangeEvent // ++
// 引入 Command
public class IncreaseCountCommand : AbstractCommand
protected override void OnExecute()
this.GetModel<CounterAppModel>().Count++;
this.SendEvent<CountChangeEvent>(); // ++
public class DecreaseCountCommand : AbstractCommand
protected override void OnExecute()
this.GetModel<CounterAppModel>().Count--;
this.SendEvent<CountChangeEvent>(); // ++
// Controller
public class CounterAppController : MonoBehaviour , IController /* 3.实现 IController 接口 */
// View
private Button mBtnAdd;
private Button mBtnSub;
private Text mCountText;
// 4. Model
private CounterAppModel mModel;
void Start()
// 5. 获取模型
mModel = this.GetModel<CounterAppModel>();
// View 组件获取
mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
mBtnSub = transform.Find("BtnSub").GetComponent<Button>();
mCountText = transform.Find("CountText").GetComponent<Text>();
// 监听输入
mBtnAdd.onClick.AddListener(() =>
// 交互逻辑
this.SendCommand<IncreaseCountCommand>();
);
mBtnSub.onClick.AddListener(() =>
// 交互逻辑
this.SendCommand(new DecreaseCountCommand(/* 这里可以传参(如果有) */));
);
UpdateView();
// 表现逻辑
this.RegisterEvent<CountChangeEvent>(e =>
UpdateView();
).UnRegisterWhenGameObjectDestroyed(gameObject);
void UpdateView()
mCountText.text = mModel.Count.ToString();
// 3.
public IArchitecture GetArchitecture()
return CounterApp.Interface;
private void OnDestroy()
// 8. 将 Model 设置为空
mModel = null;
代码非常简单,我们运行下 Unity 看下结果:
运行正确。
这样当我们,想要将 PlayerPrefs 方案替换成 EasySave 的时候,只需要对 Storage 里的代码进行修改即可。
最后给出流程图,如下:
好了,这篇就介绍到这里。
更多内容
- 转载请注明地址:liangxiegame.com
- QFramework 主页:qframework.cn
- QFramework Github 地址: https://github.com/liangxiegame/qframework
- QFramework Gitee 地址:https://gitee.com/liangxiegame/QFramework
以上是关于Unity游戏基本框架的主要内容,如果未能解决你的问题,请参考以下文章
Unity 框架QFramework v1.0 使用指南 架构篇:20. QFramework.cs 的更多内容 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏
Unity 框架QFramework v1.0 使用指南 架构篇:19. 心中有架构 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏
Unity 框架QFramework v1.0 使用指南 工具篇:09. SingletonKit 单例模板套件 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏
Unity 框架QFramework v1.0 使用指南 工具篇:05. ResKit 资源管理&开发解决方案 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏