游戏的数据有哪些类型
无非是
- 配置数据(各种道具配表里的数据,只读);
- 用户数据(玩家属性、拥有的物品,可读可写);
- 美术资源;
游戏框架需要哪些管理器
用户数据管理器
负责找到数据持久化文件,从中读取指定用户的数据,包括玩家的设置数据(音量等)、拥有的物品(金币、人物、道具)。
不需要依附游戏对象,一般是不继承MonoBehavior的单例。
配置数据管理器
负责找到配表文件,根据外界要求的资源种类、ID返回资源的详细信息(游戏内名字、图标、预制体,其他信息如价格)。
资源加载管理器
资源加载的方式有
- 直接引用(拖入场景、拖给脚本字段);
- Resources;
- AB包;
代码加载(2、3)都需要知道资源名。记录资源名的方法有:
- 脚本内硬编码;
- 配置文件记录字符串(xml、json);
- ScriptableObject可直接引用预制体,但是ScriptableObject需要知道资源名来加载;
资源加载管理器需要解决
- 多种加载方式(Resources、AB包);
- 多种资源种类(预制体、AB包、文本);
- 同步和异步加载;
发布时资源要从AB包加载,AB包里的资源不能修改,开发中使用AB包加载不方便,开发中适合使用Resources.Load或AssetDatabase.LoadAssetAtPath加载。因为Resources会被打包,发布时转换到AB包加载还要
- 把Resources改名;
- 把加载代码改成从AB包加载;
使用AssetDatabase.LoadAssetAtPath加载则不用改Resources,如果使用#if UNITY_EDITOR甚至不用改代码。让人以为这就是开发中理想的加载方式。但是AssetDatabase.LoadAssetAtPath的傻哔在于需要后缀名,而那两种不需要。
为了快速切换加载资源的方法(Resources、AB包、编辑器),除了要切换API,还要切换路径,AssetDatabase.LoadAssetAtPath还要加后缀名。(为什么每一种加载方式都有傻哔之处呢?)加后缀名可以自己封装的方法里,对typeof(T)判断类型然后加上。
注意这个脚本不能放在Editor文件夹,否则不会编译。
using UnityEditor;
using UnityEngine;
public class MyEditorResManager : MonoSingleton<MyEditorResManager>
{
public T Load<T>(string path) where T : Object
{
string suffix = "";
if (typeof(T) == typeof(GameObject))
{
suffix = ".prefab";
}
T t = AssetDatabase.LoadAssetAtPath<T>($"{path}{suffix}");
return t;
}
}
UI面板资源的加载
UI面板都是单例,每个面板有一个脚本。如果把面板资源路径硬编码写在面板脚本里,迁移面板资源时去脚本里改路径,需要重新编译一下。如果用一个面板资源配置表记录路径(json、ScriptableObject),面板资源和面板脚本类是分离的。
异步加载+多种资源种类
Android平台从StreamingAssetsPath加载资源要求必须用UnityWebRequest,它又必须异步加载。异步加载和同步加载不同的是它不是返回需要的资源,它准备好资源的时机不确定,只能传入一个委托,在资源准备好后调用。
- UnityWebRequest根据资源种类不同,得到它使用的类和方法不同。
- 通过yield return request.SendWebRequest();请求资源;
- 通过request.result == UnityWebRequest.Result.Success判断成功后,把request.downloadHandler as成对应类型取用资源;
public void LoadResUwq<T>(string resName, string abName = null, UnityAction<T> callback = null) where T : class
{
StartCoroutine(LoadResIE<T>(resName, callback));
}
IEnumerator LoadResIE<T>(string path, UnityAction<T> callback = null) where T : class
{
UnityWebRequest request = null;
if (typeof(T) == typeof(string))
{
request=UnityWebRequest.Get(path);
}
else if (typeof(T) == typeof(Texture))
{
request=UnityWebRequestTexture.GetTexture(path);
}
else if (typeof(T) == typeof(AssetBundle))
{
request=UnityWebRequestAssetBundle.GetAssetBundle(path);
}
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
if (callback != null)
{
if (typeof(T) == typeof(string))
{
callback(request.downloadHandler.text as T);
}
else if (typeof(T) == typeof(Texture))
{
callback((request.downloadHandler as DownloadHandlerTexture).texture as T);
}
else if (typeof(T) == typeof(AssetBundle))
{
callback((request.downloadHandler as DownloadHandlerAssetBundle).assetBundle as T);
}
}
}
}
public static string UwrPathProcessor(string path)
{
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
return "file://" + path;
#elif UNITY_ANDROID
return "jar:file://"+path;
#endif
return path;
}
场景管理器
提供异步加载场景的方法。显示加载界面、更新进度条的代码可以放在里面。
因为要用协程,且全局存在,应该是DontDestroyOnLoad的继承MonoBehavior的类。
声音管理器
使玩家可在任意场景的设置面板改变音乐和音效音量。游戏所有播放声音都要使用它封装的函数。它无需储存用户设置的音量,因为那属于用户数据,由用户数据管理器存储。
看想不想用拖赋值,想就用DontDestroyOnLoad的继承MonoBehavior的类。
关卡场景管理模块
以上是游戏全局需要的管理器,在关卡场景有一些所有关卡都相同的管理模块:输入模块、相机模块、UI模块(HUD、对话面板、交互选项面板、任务面板)、关卡管理器、缓存池。
这些模块一般打成一个预制体,新建关卡时直接拖入。(虚幻项目直接就有一套这些)
缓存池
在射击游戏里一般用来存弹壳、弹头、击中效果。只在关卡场景需要,所以用继承MonoBehavior的单例,可以和游戏管理器、输入模块等打进同一个预制体。
public enum BufferType{
impact,bullet, rifleShell,handgunShell,impactBlood
}
public class BufferPoolBase : MonoSingleton<BufferPoolBase>
{
public const float impactLifeTime=1;
Dictionary<BufferType,Transform> bufferDict=new Dictionary<BufferType,Transform>();
//可能的情况包括没缓冲池、有缓冲池没物体(一般不会有,但理论上可能)、有缓冲池有物体
public GameObject Depool(GameObject prefab,BufferType bufferType,Vector3 position){
GameObject instance;
if(bufferDict.ContainsKey(bufferType)){//缓冲池已建立
if(bufferDict[bufferType].childCount>0){//缓冲池里有物体
instance=bufferDict[bufferType].GetChild(0).gameObject;//取出
instance.transform.SetParent(null);//解绑
instance.transform.position = position;
instance.SetActive(true);//激活
}
else{//有缓冲池没物体(曾经放入过物体,又拿出了)
instance=Instantiate(prefab,position,Quaternion.identity);
}
}
else{//没有缓冲池
Transform bufferTransform=new GameObject(bufferType.ToString()).transform;
bufferTransform.parent=transform;
bufferDict.Add(bufferType, bufferTransform);
instance=Instantiate(prefab,position,Quaternion.identity);
}
return instance;
}
public void Enpool(GameObject instance,BufferType bufferType){
if(!bufferDict.ContainsKey(bufferType)){
Transform bufferTransform=new GameObject(bufferType.ToString()).transform;
bufferDict.Add(bufferType, bufferTransform);
}
instance.transform.SetParent(bufferDict[bufferType]);
instance.SetActive(false);
}
public IEnumerator EnpoolLater(GameObject instance,BufferType bufferType,float delay){
yield return new WaitForSeconds(delay);
Enpool(instance,bufferType);
}
输入模块、玩家人物和人物
玩家人物和NPC人物有一些共性,玩家人物还有一些个性:更新HUD、接受输入控制。通常会把玩家人物作为人物的子类,重写一些方法。
输入模块和玩家人物要写在一个脚本吗?
如果用了新的Input System,我们知道给几十个按键配回调很麻烦,会想把PlayerInput和处理回调的脚本放在一个预制体里,如果把PlayerInput和玩家人物打进预制体,那么不同关卡用不同人物时又要换人物的骨架和模型,人物身上绑的物品挂载点、约束也要重新绑。会想到写一个输入回调处理脚本MyInputHandler和PlayerInput放在一起,MyInputHandler指向玩家人物。也就是输入模块和玩家人物分离。
输入脚本和人物脚本的代码分布
人物的各种行为会封装成方法Move()、Turn()、Run()、Jump()。这些方法是在
- 人物脚本Update()
- 输入脚本Update()
- 输入脚本处理输入的方法
执行?
放在输入脚本Update()就等于输入回调先写入输入脚本字段,再传给人物脚本行为方法的输入参数。
放在人物脚本Update就等于先写入人物脚本字段再让行为方法读取。
放在输入脚本的输入回调方法就是把输入变量传给人物方法的输入参数,只在输入变量改变时执行。
为了减少字段,可以尽量用3,但有一些方法必须每帧执行,比如跑步可能被各种情况打断(射击、换弹、坠落)。跑步要执行,除了按下跑步,还要满足几个条件,这些条件有的靠状态机的互斥就可以(跳跃、坠落),有的要另外判断(主要是上半身层的)。执行跑步前把这些条件全部&&。
如何让输入在电脑和触屏之间快速切换?
输入操作可以分成持续输入(移动、旋转、跑步、射击)和瞬间输入(换弹、交互、蹲下),持续输入是维护一个变量,移动旋转是Vector2,射击跑步是bool,输入模块写入这些变量,执行模块读取,瞬间输入是直接调用执行模块的函数。那么这些变量和函数应该是独立于输入方式存在的,放在一个InputHandler类,然后这个类记录玩家,把输入传给玩家的动作。输入模块(电脑、触屏)到InputHandler之间的连接是若干种输入操作,需要把这些都连好,再加一个控制变量。

管理器分类
根据管理器的生命周期,可以分为整个游戏内存在的和场景内存在的。根据是否必须继承MonoBehavior,可以分为继承和不继承。这两个问题组合,其中不继承MonoBehavior的一定整个游戏存在。这样就把管理器分为3类:
- 不继承MonoBehavior;
- 继承MonoBehavior;
- 继承MonoBehavior且DontDestroyOnLoad;
不继承MonoBehavior
用户数据管理器、资源配置管理器;
继承MonoBehavior
各场景内的管理器。对于关卡场景,有输入、相机、秩序管理、缓存池。
继承MonoBehavior且DontDestroyOnLoad
通常是游戏全局存在且需要协程的管理器:场景管理器、AB包管理器。
还有必须和组件关联的管理器,比如声音管理器,必须记录音乐声源和UI音效声源。
这些管理器可以打成一个预制体。
登录注册系统
多用户数据系统
如果一个游戏要做登录注册界面,那么它就是一个多用户游戏,意味着它有一个记录用户名和密码的文件,而其他大部分数据(玩家拥有的人物、武器、物品、音量)都因用户而异,需要在每个用户注册时建一个文件夹,把这些用户数据文件都放在里面。
这样UserDataManager在构建的时候就不能加载所有的数据,因为还没登录,不知道用户名,也就不知道这些数据文件的路径。所以需要一个UserManager,开始运行时构建,负责获取上次登录用户、注册新用户、判断登录是否成功。加载登入用户数据的DataManager在登录成功后构建,它读取数据的路径依赖登入的用户名。


场景管理
需要有一个《场景转换图》列出所有的场景和哪些场景。一般需要登录场景、主界面场景、若干游戏场景。登录场景和主界面场景应该分开,因为主界面场景可能从登录场景或游戏场景进入。
MVC框架
有一个处理数据的模块M,每个面板有一个显示模块V和控制模块C。
M负责
- 数据从持久文件读取、修改、保存回持久文件;
- 数据改变时通过维护的事件执行,影响外部,它不知道会影响外部的什么,可能引起UI刷新或不引起;
- 数据为私有,开放公共属性,修改数据只能按特定的方法的规则;
C模块
- 在Start里把刷新自己面板的方法加入M模块数据修改后执行的事件;
- 在OnDestroy里把刷新自己面板的方法移出M模块数据修改后执行的事件;
MVP(Presenter)架构
V脚本只有控件引用,Presenter负责给V的控件引用赋值更新UI,V和M直接没有引用。
9206

被折叠的 条评论
为什么被折叠?



