unity小技巧

本文分享了Unity游戏开发中实用的技巧,包括项目管理、资源组织、预设使用、单例模式等多个方面,帮助开发者提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


关于这些技巧
这些技巧并不是适用于每一个项目。

    1.基于我的经验它们适用于3到20人的小团队。

    2.一些结构性,重用性,清晰度等等上的技巧使用需要付出性能上的开销代价,根据你团队和项目的大小来决定是否需要付出这些开销代价。

    3.许多技巧的选择可能会有自己不同的喜好(它们可能有对比,但是这里列出来的都是可用的好技术)

    4.一些技巧可能和 unity官方提倡的用法大相径庭。例如,规模化的使用预设相对于使用实例来说是Unity官方不提倡的,开销的代价非常大(使用大量预设的时候)。然而我看到这些技巧能够带来很好的结果,即使它们看起来很疯狂。

今天将给大家介绍前5个技巧,Are you ready?

进程
1.避免资源分支。任何之源都应该只能有一个版本。如果你一定需要分支预设,场景或者网格,那就遵循一个清晰的进程已确定正确版本。“错误”分支都有个扯淡的名字,例如,使用双下划线作为前缀:__MainScene_Backup.预设分支需要一个特定的流程保证它们使用的安全(请看下面的预设部分)。

2. 每个团队成员都应该拷贝一份工程出来当做测试使用如果你们使用项目版本控制。工程有更改时,这份清晰的拷贝工程要及时更新测试。没人应该去更改清理的拷贝工程。这对解决丢失资源非常有用。

3.考虑使用外部关卡编辑器来编辑关卡。Unity不是一个完美的关卡编辑器。例如,我们使用TuDee来创建关卡编辑,这受益于我们使用tile-friendly工具(拍摄网格,多个90度的旋转, 2D视角)。直接从XML文件初始化预设。关于  Guerrilla Tool Development的更多信息请访问原文。

4.考虑把关卡信息储存在XML文件中而不是在场景里。这是个很棒的技巧:
      1.这样可以避免在每个场景都重构数据。
      2.这样加载更快(如果大多数场景对象之间共享)。
      3.这样更容易合并场景(即使使用Unity新特征中的基于文本的场景,无论如何仍然有大量的数据在融合时不好实行)。
      4.这样更容易追踪跨级别的数据。
你仍然可以把Unity当做关卡编辑器使用(尽管你不需要。)你需要写一些代码序列化和反序列化你的数据,在游戏编辑和启动的时候加载关卡数据,并在关卡编辑器中储存数据。你可能还需要模仿Unity的ID系统来维护对象之间的引用。

5.考虑使用泛型定义检查器代码。编写自定义检查器十分简单,但是Unity的系统有许多弊端:
      1.不支持利用继承。
      2.不支持定义一个字段类型的检查器组件,只支持类类型。例如,如果每个游戏对象都有一个字段类型SomeCoolType,你想让它在检查器面板中不同的显示,你就必须在你的所在类里写检查器。
       你可以从底层重新实现检查器系统来解决这个 问题。使用反射的一些小窍门,这并没有看起来那么困难,细节在文章的最后章节有提供。

组织场景
6.使用命名为空的游戏物体作为场景文件夹。仔细组织你的场景便于更容易地查找相关对象。

7.把相关预设和场景文件(empty game objects)放在(0, 0,0)点。如果一个transform不是专门用来确定物体的位置,那么应该把它放到远点。那样可以在处理当前和世界坐标系的问题时少出问题,而且代码十分简单。

8.减少使用有偏移量的GUI组件。偏移量只应该在父容器里布局组件时使用;不应该让它们依靠祖父类来定位。偏移量不应该取消正确的相互显示。它基本上时为了防止类似以下这种情况:父容器任意定位到(100,-50)。子类,应该定位在(10,10),然后相对于父容器来说就定位在(90, 60)。当容器不可见或者无可视化表示的时候这种错误很容易出现。

9.把你的世界场景地板坐标 y值设为0。这样把对象放到场景地板上时会更加简单,游戏逻辑,AI和物理特性可以把它们当做2D场景世界来处理。


10.运行游戏的每个场景。这大大的减少了测试时间,让所有的场景都运行你需要做两件事:首先,提供一种方法来模拟之前需要加载的不可用的场景数据。其次,对象的产生必须保持在场景加载中,看一下代码:

?
 
1
2
3
myObject = FindMyObjectInScene();
if (myObjet == null ){  
myObject = SpawnMyObject();
}
Art 11.把角色和站立对象的基准点放在底部,而不是中间。 这样更容易把角色和对象精确地放到场景层上。也更容易在游戏逻辑、AI甚至物理特性上适当的当做2D方式来处理。
12.让所有网格的朝向保持一致(Z轴正向或负方向)。这适用于类似角色和其他有朝向概念的对象网格。如果所有物体的方向保持一致可以简化算法。

13.一开始把scale属性设置好。可以把它们的缩放因子scalefactor设置为1,然后把transforms的缩放设置为1,1,1。使用一个对象引用(比如一个立方体)可以让缩放比较更容易。

14.做一个plane 用作GUI组件或手动创建粒子。让plane面向Z轴正向可以更简单的使用公告牌属性、更便捷地生成GUI。

15.制作和测试美术资源  
      1.给场景加入天空盒。
      2.网格。
      3.各种纯色的shader测试:白色,黑色,50%的灰色,红色,绿色,蓝色,洋红色,黄色,青色。
      4.渐变色的shader测试:黑到白,红到绿,红到蓝,绿到蓝。
      5.白色和黑色的调色盘。
      6.平滑和粗糙的标准贴图。
      7.测试场景的光照快速设置。
http://www.gameres.com/657808.html
预设
16.所有资源使用预设。在场景里唯一一个不使用预设的游戏对象就是文件夹。即使只使用一次的特别对象也应该使用预设。这样需要更改的时候只需要改变更改预设而不需要改变场景。(一个额外的好处是,使用EZGUI创建精灵集时更加可靠)
17.使用单独的预设组合专业化,不要专门研究实例。如果你有两种敌人类型,只是属性上的不同,使用独立预设区分它们的属性,并把它们联系起来。这样做有可能
      1.在一个地方就可以对每个类型进行修改
      2.更改时没必要修改场景

18.连接预设;不要连接实例。把预设拖入场景中的时会维持预设间的联系;连接实例则不会这样。任何时候尽可能的连接预设会减少场景的构建以及减少改变场景的需求。
19.尽可能让实例之间自动建立连接。如果你需要连接实例,以动态程序简历连接。比如,玩家预设在开始的时候能通过GameManager来注册,或者可以通过GameManager找到玩家预设实例。
如果你想添加其他脚本就不要网格添加到预设的底层。当你使用网格构建预设的时候,用一个空的游戏对象作为网格父类,并让它作为最底层根对象。然后把脚本放到这一次,而不是放到网格节点那一层。这种方法可以让你更方便的替换网格而不会失去任何你在inspector中创建的值。
使用预设链接替代嵌套预设。Unity不支持嵌套预设,然而第三方解决方案放到团队工作中时又可能存在一定的安全隐患,因为与嵌套预设间的关联不明显。
20.对分支预设使用安全进程。以下使用玩家预设作为例子来说明。
对玩家预设做出的风险修改如下:
     1.复制Player预设。
     2.把复制的玩家预设重命名为__Player_Backup.
     3.修改Player预设。
     4.如果一切正常,删除__Player_Backup.
不要命名复制预设 Player_New,对它进行修改!
有些情况更复杂。例如,某个修改可能会涉及两个方面,参照上面的进程可能会破坏运行的场景除非做到两点:
    1.方法1:
            1.复制Player预设.
            2.重命名为__Player_WithNewFeature或者__Player_ForPerson2.
            3.修改复制的预设,然后提交/给 方法2
    2.方法2:
            1.修改新的预设。
            2.复制Player预设,然后重命名为__Player_Backup.
            3.拖动一个__Player_Backup和__Player_WithNewFeatrue的实例到场景中。
            4.拖动实例到原件Player预设。
            5.如果一切妥当,删除__Player_Backup和__Player_WithNewFeatrue.
21.扩展继承至MonoBehaviour的创建类,然后获取所有的组件。
这允许你实现一些基本功能,如安全类型调用和其他更复杂的调用(比如随机等)
22.定义StartCoroutine和Instantiate的安全方法调用。
定义一个委托任务,使用委托代替字符串名字来定义方法。例如:
[C#]  纯文本查看  复制代码
?
 
1
2
3
public void Invoke(Task task, float time)
{  
      Invoke(task.Method.Name, time);
}

23.使用扩展和组件共享一个接口。有时候为了方便某个组件实现接口,或者通过类似组件寻找对象。使用typeof替代通用版本的这些函数来实现。通用版不使用接口,但是typeof可以。下面的这些方法可以整洁得覆盖通用方法。
[C#]  纯文本查看  复制代码
?
 
01
02
03
04
05
06
07
08
09
10
publicstatic List<I> FindObjectsOfInterface<I>()where I : class {
    MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
    List<I> list = new List<I>();
  
    foreach (MonoBehaviour behaviour in monoBehaviours){
       I component = behaviour.GetComponent( typeof (I)) as I;
  
       if (component != null ){
          list.Add(component);}}
  
    return list;}

24.使用扩展让语法更便捷。例如:
[C#]  纯文本查看  复制代码
?
 
1
2
3
4
5
6
7
8
public static class CSTransform
{
   public static void SetX( this Transform transform, float x)
   {
       Vector3 newPosition = new Vector3(x, transform.position.y, transform.position.z);
       transform.position= newPosition;
   }
   ...
}

25.使用GetComponet获取范类型替代。有时候迫使组件依赖关系(通过RequiredComponent)是一种痛苦。例如,使得在inspector中更改组件变得更困难(即使他们有同样的基本类型)。作为一个选择方案,下面的GameObject扩展,在找不到组件并需要打印它的错误信息时使用。
[C#]  纯文本查看  复制代码
?
 
1
2
3
4
5
6
7
public static T GetSafeComponent<T>( this GameObject obj)where T : MonoBehaviour
{
    T component = obj.GetComponent<T>();
  
    if (component == null ){
       Debug.LogError( "Expected to find component of type " + typeof (T)+ " but found none" , obj);}
  
    return component;}


26.避免使用不同的方案完成同样的事情。在许多情况下不止一个惯用方法来处理事情。在这种情况下,选择一个在项目中使用,原因如下:       
1.一些方案并不适合放到一起。在一个方向上使用一种方案来做设计后,可能在使用其他方案就不合适。
2.至始至终使用同一方案让团队成员对此的理解更加简单。对工程的构建和代码实现也更容易理解。这样一来就不容易犯错。
方案组示例:
        1.协同 vs 状态机。
        2.嵌套预设 vs 连接预设 vs 神预设。
        3.数据分离策略。
        4.在2D游戏里使用精灵的方式。
        5.预设结构。
        6.生成策略。
        7.定位对象的方法:用 类型 vs. 名字 vs. 标签 vs.层级 vs.引用(“链接”)。
        8.编组对象的方法:用 类型 vs. 名字 vs. 标签 vs.层级 vs. 数组引用(“链接”)。
        9.用寻找编组对象与对象注册机制。
        10.控制执行顺序(使用unity的执行顺序机制与yield逻辑,依赖Awake/Start和Update/LateUpdate一系列方法与方法指南以及其他顺序排列架构)。
        11.在游戏中用鼠标选择objects/positions/targets:选择管理器与自我管理。
       12.场景切换时保留了数据:通过PlayerPrefs,或者当加载一个新场景时对象没有被销毁。
        13.动画相结合的方式(混合,添加和分层)。

时间
27.保持自己的时间类来简化暂停功能。封装Time.DeltaTime和Time.TimeSinceLevelLoad来计算暂停和time scale.按照需求酌情使用,会让开发更加简单,特别是执行多个时间系统时(比如动画接口和游戏游戏动画)。

生成对象

28.不要在游戏运行时生成对象来打乱你的层级。[size=+0]游戏运行时给场景中的对象设置父容器能更容易找到对象。你可以使用一个空的游戏对象,甚至或者一个没有默认基类的单例都可以很容易的访问代码。我们把这种对象叫做DynamicObjects. 类设计

29.方便使用单例。下面的类将会使任何类继承至它的单例:
[C#]  纯文本查看  复制代码
?
 
01
02
03
04
05
06
07
08
09
10
11
12
publicclass Singleton<T>: MonoBehaviour where T : MonoBehaviour
{protectedstatic T instance;
 
/**
Returns the instance of this singleton.
*/ publicstatic T Instance
{ get { if (instance == null ){
instance =(T) FindObjectOfType( typeof (T));
 
if (instance == null ){
Debug.LogError( "An instance of " + typeof (T)+ " is needed in the scene, but there is none." );}}
 
return instance;}}}

单例模式对于管理器非常有用,比如ParticleManager或者AudioManager又或者GUIManager.

        1.避免对不是管理器的单一预设实例使用单例(比如Player).不遵守这个原则会让继承层次变得复杂,改变某个类型也会更加困难。宁愿在GameManager里保持这些引用(或者其                    他合适的神类别 )
        2.定义静态属性和方法,外部会经常调用这些公共变量。你可以用GameManager.Player代替GameManager.Instance.player.
30.对于组件,绝对不要设置公共变量,也不应该在inspector中去调整。否则就会被设计者调整,特别是在不清楚它要干什么的时候。但在一些罕见的情况下这是不可避免的。在这种情况下使用两个甚至或者四个下划线的前缀的变量名来修饰:
[C#]  纯文本查看  复制代码
?
 
public float __aVariable

31.从游戏逻辑中分离接口。这个本质是MVC模式。
任何输入控制器只应该给适当的组件发送命令,让它们知道控制器已经被调用。例如在控制器逻辑中,控制器可以根据玩家状态决定发送哪些命令。但是这样也不好(例如,如果添加更多的控制器会导致更多的重复逻辑)。相反,玩家对象应该通知向前移动,然后基于当前状态(例如放缓或不知所措)设置速度,更新玩家朝向。控制器只做涉及他们自身状态的事儿(如果玩家改变状态,控制器不会改变状态,因此,控制器根本不应该知道玩家的状态)。另一个示例是改变武器。正确的做法是用GUI访问Player的一个方法SwitchWeapon(Weapon newWeapon),GUI不应该操作transforms和父类和其他所有东西。
任何组件接口都只应该维护与它自己状态相关的数据和进程。例如,显示一个地图,GUI能根据玩家的移动计算怎么显示。然而,这是一个游戏状态数据,不属于GUI。GUI仅仅应该只显示任何应该保留的游戏状态数据。而地图数据应该在任何地方都保留(例如在GameManager里面)。
游戏对象几乎对GUI一无所知。暂停行为除外,它可以通过Time.timeScale控制全局(这不是一个好主意,看吧)。游戏暂停的时候所有游戏对象应该能知道。因此,没有游戏对象和GUI组件连接。
一般来说,如果你删除所有的GUI类,游戏仍然能够编译。你也应该能够重新实现GUI和输入,而不需要编写任何新的游戏逻辑。
32.状态分离和记录。记录变量对快速便捷的开发很有用,也可以恢复到之前的状态。通过分离,你能更便捷的实现:
    1.储存游戏状态
    2.调试游戏状态
实现它的一个方法是给每一个游戏逻辑类定义一个SaveData类
[C#]  纯文本查看  复制代码
?
 
1
2
3
4
[Serializable]
PlayerSaveData
{publicfloat health; //public for serialisation, not exposed in inspector}
 
Player



33.独立的专业化配置。
考虑到两种敌人都使用了完全相同的网格,但是不同的扭力(例如不同的力和不同的速度)。有两种方法可以分离数据。有一种方法是我很喜欢的,特别是当有对象生成时,或者游戏储存时(扭力不是状态数据,而是配置数据,所以它不需要被储存。当对象加载或生成时,扭力数据会自动独立加载)
       1.给每个游戏逻辑类定义一个模板类。例如,Enemy类,我们也定义一个EnemyTemplate.所有不同类型的扭力值都被储存在EnemyTempate里
       2.在游戏逻辑类中,定义一个模板类型变量。
       3.创建一个敌人预设,两个模板预设WeakEnemyTemplate和StrongEnemyTemplate.
       4.当加载或者生成对象时,设置模板变量为正确的模板
这个方法十分复杂(有时没必要这么复杂,所以要小心)。
例如,更好的利用通用的多态性,我们可能这样定义我们的类:
[C#]  纯文本查看  复制代码
?
 
01
02
03
04
05
06
07
08
09
10
publicclass BaseTemplate
{...}
 
publicclass ActorTemplate : BaseTemplate
{...}
 
publicclass Entity<EntityTemplateType>where EntityTemplateType : BaseTemplate
{
EntityTemplateType template;...}
 
publicclass Actor : Entity <ActorTemplate>{...}




34.不要使用字符串显示文本。特别是不要使用字符串识别对象或者预设等。但是动画例外,通常用字符串访问它们。
35.避免使用公共索引链接的数组。例如,不要定义武器数组,子弹数组和特效数组,你的代码看起来应该如下:
[C#]  纯文本查看  复制代码
?
 
1
2
3
4
5
6
publicvoid SelectWeapon( int index){
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);}
 
publicvoid Shoot(){
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);}


问题主要不是用代码解决,而是在inspetor中正确设置。

相反,定义一个类,封装了三个变量然后创建一个数组:
[C#]  纯文本查看  复制代码
?
 
1
[Serializable]publicclass Weapon
{ public GameObject prefab; public ParticleSystem particles; public Bullet bullet;}



代码看起来很整洁,但是最重要的是,在inspector里设置数据会更加简单。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值