- 最后登录
- 2018-6-4
- 注册时间
- 2012-12-15
- 阅读权限
- 90
- 积分
- 52052
- 纳金币
- 25
- 精华
- 1
|
是时候激谈论用Unity开发游戏的过程了。我喜欢Unity,不可能再去研究其他的游戏引擎。也就是说有好的部分也有些不好的部分。下面主要是在处理框架项目时候的一些信息。我当前正在写一个框架,便于以后开发新游戏项目时使用。主要就是为了写一些功能类,因为我不想每次都重新写,并且有了框架后也会让我的文件结构清晰点,不至于乱糟糟的。
管理器
在任何游戏里,管理器(单例)都占了很大一部分。我怀疑能否在制作游戏时候一个都不用或者只用一个管理器。的确,Unity很明显有更加清晰的方式来使用单例。这样来看就简单多了,但是我喜欢在写一些场景中不存在的管理器,虽然在其他东西的外面但是仍然会接收重要的诸如更新等功能的事件。
我最终就是要为单例写一个模版基类。它会追踪静态实例,调用DontDestroyOnLoad等。在获得实例的时候,它实际在后面做了如下的很多事情。
1. 尽量只返回实例
2. 尽量用FindObjectOfType来找到场景中的单例。
3. 使用对应类型名字的Resources.load来找到预制品。我是在AudioContoller上使用这种方式,因为我需要一个提前配置值的预制品。
4. 最后如果失败,就创建一个新的游戏对象,然后添加组件。
网络
最大的优点在于它有一个统一的网络结构。网络是紧密结合到脚本引擎中的,那样程序员就可以像编写单播放器脚本一样编写多播放器脚本,而不需要额外工作。
Unity的网络API工作起来不怎么如人意。虽然增加或者删除一个网络视图不会对游戏脚本产生影响,但是当存在单播放器和多播放器时,RPC和对象实例化就很难应付了。
我的解决方案就是写一个网络管理器来实现很多有用的功能:
· 通过名字或者索引加载等级。当网络工作时,会自动咱提网络队列的处理,当等级加载完毕后就可以通过固定一个回调函数
· 实例化对象。如果断开,就使用通常先前的GameObject.Instantiate函数,否则就用NetWork.Instantiate来实例化对象。
· 消除对象。如果断开,就使用GameObject.Destroy,否则使用Network.Destroy。
另外还需要为所有组件创建一个基类-GameBehavior。此类有封装了一个关于networkView.RPC的封装器,用来在没连上服务器时控制RPC。另外内置支持组件缓存和多态支持。
关于网络最后一点:另外需要写几个类来封装网络特性。它们是:NetFloatProperty,NetVector2Property,NetVector3Property,NetVector4Property。每个类都可以从OnSerializeView读取位流和调用netFloatWrapper.Add,然后在更新中可以调用netFloatWrapper.GetValue()来自动篡改进入的数据包。
音频
Unity在音频上还是比较弱的,因此别被主页上的广告忽悠了。可以说Unity的音频脚本系统知识个骨架。基本上99.999%的游戏都需要音频通道或者音频组系统。包括音乐,声音,音效以及气氛乐等给定声音。但是Unity却没有此概念。根据以前有限的使用XNA的XACT工具我可以说在提示跟摘录的声音之间添加一个抽象层是很有必要的。当枪响时就可以播放枪响的提示。提示本身可以能有多种不同设置,例如音量,是否循环,是否随机截取或者按照固定顺序步进。它包括大量音效截取,每个部分都包括音效和相关设置。
知道上面的知识后,我准备使用类似概念来编写自己的音频系统。我的AudioController有大量AudioCategory。每个AudioCatego都有大量AudioItem。而每个AudioItem又有大量SubItem。因此如果我想播放SFX目录里一个名为“Gunshot”的AudioItem,我可以如下调用:
AudioController.PlaySound( "SFX.Gunshot" ); //播放音效,自动回到起始处。
AudioController.PlaySound( "SFX.Gunshot", transform ); //从给定的转换通道播放音效,会根据通道运行。
AudioController.PlaySound( "SFX.Gunshot", transform.position ); //从特定位置播放音效,而不是从特定位置移动。另外如果我想播放一首Music目录下的歌曲,我可以调用PlayMusic(“”)。歌曲也可能会有多个不同通道。如果音乐已经在播放,PlayMusic会自动继续播放。
对于结果我感觉很满意,当然需要设计更加友好并且高效。
输入
不要误解哦,我是喜欢Unity的输入系统的。但是还是有很多需要改进的地方。对新手来说不可能用默认系统来配置游戏的输入。另一个问题就是很多设备没法充分利用输入系统。例如在移动设备上,需要自己编写基于触屏的输入系统,因为Unity对于触屏API的支持还不是很多。
而我要做的就是写一个Gamepad类来提供标准的GetAxis/GetButton等映射Unity输入的功能。同时也提供了一个输入接口来便于编写嵌入的输入层。现在可以写一个用于测试的Unity输入层,主要面向移动游戏的输入层。当然如果面向OUYA,也可以为OUYA开发者写一个输入层工具。可以写一个用于插口的输入出来方便配置输入,我的代码都是调用,没必要知道所有东西。直接通过Gamepad.GetAxis(“MoveX”)调用即可。
此外又添加了一个简单但是很有帮助的功能:Gamepad.LockInput。平时基本都设置LockInput为true,这样所有输入都被锁定。例如,如果有一个多播放器游戏,播放器弹出菜单,此时无需暂停游戏,也无需让玩家在菜单弹出时还继续玩游戏。因此可以见到将LockInput设为true这样播放器自动回在特定位置固定。
游戏保存
我以前没接触过有游戏保存功能的游戏。但我觉得我需要一个,并且Untiy也没有此功能,因此可以为游戏本身写一个简单的游戏保存功能。
主要想法:如果设置这样的限制:所有需要保存的东西都保存在一个资源文件夹内,那就可以在加载游戏的时候简单的保存原来的内容路径,然后初始化尼尔并还原到原来的状态。
此时需要创建一个ISaveable接口,此接口有两个功能:保存和加载。当保存游戏时,会遍历所有实现ISaveable的类来查看所有游戏对象。这些内容都保存在一个独立的列表中。对象序列化如下:
加载等级ID
序列化游戏对象数量
[每个游戏对象]
内容路径
位置
循环
大小
ISerializable组件数量
[每个ISerializable]
GetType().Name
ISerializable.Save(Stream)
这样在加载保存的游戏时,通过Resource.Load加载等级和每个物体的实例化,设置位置、循环、大小,然后用GetCompone(保存名字)获得每个ISerializable数据,强制转为ISerializable,最后调用ISerializable的Load函数即可。
将上述都封装在SaveManager类的多个功能函数里,每个函数功能如下:
GetSaveSlots() //获取当前保存的游戏文件,可以直接传给LoadGame,并保存在PlayerPrefs里。
SaveGame(SaveID) //将游戏保存到特定槽内。如果文件已经存在,将会重写,否则会新建一个。
LoadGame(SaveID) //从特定保存槽加载游戏。
DeleteGame(SaveID) //删除给定的保存槽,文件和入口都会从PlayerRefs清除。
但是需要注意在Web播放器上是不会工作的,因为Web上需要大量使用诸如文件流等IO函数,但目前为止,在其他平台还是可以的。
哈,还是经过漫长的学习啊,我也确信大多数人会从此文中获得些东西,我只是与别人分享关于Unity的一些经验,通过编写游戏我发现自己越来越喜欢Unity了。 |
|