Develop

以前我扩展 Button 组件的方式是再写一个组件,然后在给 Button 组件加上 Event Trigger 组件去处理事件,比如 PointerUp 等。

后来我发现更好的方式是继承 Button 组件,或者继承 Selectable 组件,然后重载方法。除了做事件响应外,比如做子对象UI的颜色变换等,也非常方便,

比如做需要做一个可拖拽按钮,可以新建一个类 DragableButton : Selectable, IDragHandler . 然后实现 IDragHandler.OnDrag() 方法即可。

有很多技术其实都不能称之为技术,比如 Unity 各种 api 的使用方法,但要深入一线开发,又不得不去掌握这些东西。所以干脆就写一篇文章,全部总结一下。

阅读更多

前一阵子看到一个 Unity 教学 youtuber 在用状态机做角色控制的时候,竟然直接使用 Animator Controller (以下称为动画状态机)作为角色逻辑状态机,这是我从没想过也不敢尝试的方法。

我也喜欢用状态机来做角色控制,但一般是自己写一个逻辑状态机,然后再用这个状态机去确定动画播放。动画状态机对我来说只是一个播动画的组件,即使哪天 Unity 出了新的动画系统也可以随时把它换掉。

其实以前在做项目的时候,我曾对于动画状态机和自己写的逻辑状态机如何协同工作感到困惑。如果动画状态机也使用一些 conditions 来进行状态切换,那么动画状态机和逻辑状态机很可能会出现不一致的情况。这在做网络游戏的时候尤其明显,即便服务端和客户端同步了逻辑状态机,动画状态机的状态由于一些 timing 的差异也会不一致,除非再单独同步动画状态机,也就是逻辑状态和动画状态各自单独同步,但我觉得这个方案并不合适(也考虑过只有服务端使用动画状态机,客户端用类似 legacy 动画系统的方法来播放动画,也不好)。

后来得出的方案是一切以逻辑状态机为准,逻辑状态机决定动画播放,即使没有动画系统,逻辑状态机也能独立完成整个游戏逻辑。这样如果想要动画系统表现好,逻辑状态机应该知晓一些动画的数据信息,这些信息是在编辑器阶段(而非运行时阶段)就准备好的,比如逻辑状态机和动画系统都应该知晓攻击动画的持续时间是多久,而不互相依赖。为了网络同步,动画系统应该能从任意动画切换到任意动画,这样动画系统实际退化成了一个简单的动画播放器。

我想应该很多网络游戏都是这样做的。

但如果是一个单机动作游戏呢?如果逻辑高度依赖动画系统,尤其是一些近几年兴起的 physics based 动画系统呢?这时动画系统还是需要参与逻辑运算。而开头说的那位 youtuber 更激进了一步,直接将动画状态机作为逻辑状态机,以致于理论上来说,某些动画状态机节点甚至可能不播放动画。以前 StateMachineBehaviour 刚出的时候我以为只是用来做一些简单的动画计算,没想到现在可以用来运行核心逻辑。

我本来也不是很确定像他这样用会不会有什么问题,但后来看到 CinemachineStateDrivenCamera 组件里,Unity 将 Animator Controller 作为不播放动画的纯状态机来使用,我想 Unity 是倾向于这样使用的。

游戏制作过程中难免会涉及到使用一些数据表。

很多游戏对数据表的使用方法是 用游戏对象去依赖数据表,比如初始化血量、攻击力等数值时,去根据相关表格进行计算。

但我个人更倾向于使用 数据表依赖游戏对象 的方法,游戏对象完全不依赖数据表的存在。具体的做法是:数据表中的数据读写,实际上是对游戏对象数据的读写,数据表只是相当于把多个游戏对象的数值汇总,以用于统一管理。

在 Unity 里我习惯使用 ScriptableObject 来作为数据表,这样的话引用 prefab 会很方便。给数据表的 property 加上 Odin InspectorShowInInspector, 就可以实现通过 ScriptableObjectprefab 进行读写。

Prefab 是可以这样做了,但是由于场景中的对象无法在 ScriptableObject 中序列化,所以对于场景对象的修改还得想想办法。

我目前的办法是在场景中放一个对象 M, 用于存放场景中所有需要通过数据表进行修改的对象引用。每次使用数据表时,把 M 拖到数据表中进行数据表中引用的初始化,这样就可以了。

实际上大部分人大概都不是使用 ScriptableObject 来做数据表,而是使用例如 Excel, CSV 之类的表格。这些表格不能读取项目中游戏对象的实际参数,这时我认为应该使用 ScriptableObject 作为中介者,读取表格的数据,写入各个对象。

上面我所说的,都是针对游戏开发阶段。如果是 Stellaris 这类需要在发布后依赖外部表格的游戏,或者是玩家可以通过表格进行自定义修改的游戏,那么需要重新考虑问题。

大部人可能为认为做这些是多此一举,但我写工程习惯于简洁直接地表达事物之间的关系。在整个游戏逻辑里,数据表的概念实际上是不需要的,实际必要的是游戏对象的数值。数据表只是开发者开发时的中间产物,那么它就不应该出现在最后的发布版里。

今天在写 C# 的时候遇到下面这样一个问题。

首先我们大多数人知道,如果有一个静态类像这样:

1
2
3
4
5
public static class c1
{
public static int v1 = 256;
public static int v2 = v1;
}

然后打印 v2 会得到 256。

而如果它是:

1
2
3
4
5
public static class c1
{
public static int v2 = v1;
public static int v1 = 256;
}

然后打印 v2 会得到 0。

这是由它声明顺序决定的。

但是如果静态成员变量 v1, v2 在不同的类中,情况就没有这么简单。如果有这么两个类:

1
2
3
4
5
6
7
8
9
public static class c1
{
public static int v1 = 256;
}

public static class c2
{
public static int v2 = c1.v1;
}

此时不管是以上面这样的顺序声明两个类,还是换一下顺序:

1
2
3
4
5
6
7
8
9
public static class c2
{
public static int v2 = c1.v1;
}

public static class c1
{
public static int v1 = 256;
}

打印 c2.v2 的结果都是 256。

这时我就纳闷了,就跟朋友们讨论了一下,他们说可能是因为编译器很智能,会根据类之间的依赖关系来决定初始化顺序。那么就有新的问题了,如果他们相互依赖的话,情况又是怎么样呢?

为了测试,把两个类改成这样:

1
2
3
4
5
6
7
8
9
10
11
public static class c1
{
public static int v1 = 256;
public static int v2 = c2.v1;
}

public static class c2
{
public static int v1 = 1024;
public static int v2 = c1.v1;
}

此时先后打印 c1.v2, c2.v2, 会得到结果:

1
2
1024    // c1.v2
0 // c2.v2

即使我调换了 c1, c2 的声明顺序,也是得到这个结果。

但是,如果我换一下打印顺序,先打印 c2.v2, 再打印 c1.v2, 结果是:

1
2
256    // c2.v2
0 // c1.v2

也就是说,在这种情况下,会根据代码的执行顺序来决定初始化顺序。不会报错,也不会报警告。

而这通常不是我们想要的。在编码过程中要尽量避免把代码写成这种情况,我们不应该依赖智能的编译器。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×