咨询电话:400-810-1418服务与监督电话:400-810-1418转接2

分享7天速成一款RTS游戏的经验

发布时间:2018-11-17 19:57:24

作者:Ferdinand Joseph Fernandez

我参加“7天速成RTS”(7dRTS)游戏制作活动完成的《Strat Souls》是一款简单的多人迷你战斗RTS游戏。

如何制作像即时策略游戏那么复杂的东西?特别是当只有你一个程序员负责所有东西,而且要在7天内完成?

我的做法是使用标准软件工程原则。

Strat Souls

这个思路就是把类从低级细节分成高级概念。

我通常用一些术语如封装、抽象、松耦合等解释这个过程。但我还会加上例子。

以下是一个单位的一组类,你会看到有不少。

最低级类

这些类直接与Unity系统(游戏的引擎)相关。一个类只做一件事。

通常来说,除了这些不会有其他职业直接调用Unity的内部结构(但也有例外)。

UnitMovement类:负责移动和旋转单位。这是唯一直接控制单位的刚体的类。还处理碰撞和避开障碍。

UnitAnimation类:控制Mecanim(通过动画类)的唯一的类。Mecanim是Unity的动画系统。

UnitNetwork类:处理NetworkView和Network的唯一的类,即为Unity发送和接收网络数据。对于单位,这是具有RPC(远程过程调用)和网络状态同步的唯一的类。

Health类:管理单位的命值,非常基础的代码。

……

这些类是沟通Unity的系统和我的代码的桥梁。那意味着我的代码或多或少不是直接访问Unity类库(除了前面提到的那些)。

strat souls

基本的问题域级类

注:这些名称是我自己编的。

在最低级之上的是基本的问题域级。它利用最低级的类(准确地说,是通过它们提供的公共功能)来制作更高层次的便利功能。就问题域而言,在其之上的层次继续分类。

但“问题域”到底是什么?因为我的游戏是一款RTS,所以我的功能是按照TurnToTargetUnit()构建的,而不是RotateToVector()。

我现在是按照RTS游戏思的,不是基础的3D系统。那是因为我的问题域是即时策略。所以我的代码是根据“即时策略”的概念写的。它提供的功能允许调用者不需要知道底层3D世界和物理系统,或至少不是那么需要。

这一级只包含一个类,就是Unit,它作为各种最低级类的外观类。没有继承性。Unit只有已存在的UnitMovement, UnitAnimation, UnitNetwork等的变量。

Unit类有公共功能如:

MoveToPosition或MoveToUnit:使用UnitMovement移动,使用UnitAnimation播放移动动画。

DoMeleeAttack:播放近战动画(通过UnitAnimation),开启近战碰撞。当处于多人模式时,它还保证这个活动通过网络重复播放(通过UnitNetwork)。

IsNearPosition或IsNearUnit:使用UnitMovement检查某个单位是在附近位置。

IsFacingUnit:使用UnitMovement检查某个单位是否面对另一个单位。

IsNearAndFacingUnit:组合这两个单位的便利功能。

IsDead/IsAlive:使用Health类来检查某单位是生或活。

GetAllEnemiesNearMe:返回在某个范围内的敌人单位数列,并按接近程度分类。

……

如你所见,Unit只是顺从最低级类。Unit就像乐队中的指挥,他并不演奏任何乐器,但协调其他演奏乐器的人。他告诉他们该做什么,什么时候做。

strat souls

中级类

在基本的问题域级上添加基本的行为。

当你右击某物时,中级现在直接对应单位的活动。因为我希望不同类型的单位做不同的事,所以我把这每一个类都分开了。

所以单体可以做不同类型的Action。我创建一个抽象的基类Action,Unit类有两个Action变量,当你左击地面时,它使用一个变量;当你右击敌人时,它使用另一个变量。

Action有不同的亚类:

ActionMove:

只会移动到被要求的目的地(使用Unit.MoveToPosition)。

注意,只需要一次功能调用,Unit类就既能处理移动物理,也能负责播放移动动画。

ActionMelee:

单位移动到距离目标敌人足够近的地方(调用Unit.MoveToUnit),一旦单位到达目的地(游戏邦注:继续查看Unit.IsNearAndFacingUnit的返回值),就会保持攻击状态(调用Unit.DoMeleeAttack)直到目标敌人活亡(查看Unit.IsDead的返回值)。

另外,注意调用所有那些功能会保证动画正确播放、通过网络的移动和攻击保持同步,我们不用担心不能达到同步,因为最低级能处理同步问题。在这一级,我们只关心单位的行为,而不是低级细节如设置速度或协调两段动画。

还有其他东西如ActionRangedAttack或ActionDashAttack(在我的游戏中,这是Bone Wheel使用的),也许还有ActionBuffAlly,等等。

所以现在我通过组合Unit类提供的公共功能得到更高级的行为。我的Unit类代码组合不同的低级工具最终做出实用的东西。

另外,我可以给单位赋上不同的Action。这意味着我可以通过交换近战单位使用的Action,把它转变成远程单位。

注:那些Action类如何知道要求的目的地或目标是什么?这是由我的Order singleton类负责的。它只要知道鼠标光标所在的3D X、Y、Z位置,或鼠标控制的单位,它就会与UnitSelector singleton交流,把命令告诉所有当前选中的单位(当检测到右击事件时)。但当命令发布时,各个单位到底做什么,取决于它使用的Action亚类。

最高级类

添加玩家通常期望的行为。在这短短的7天里,我其实没有时间执行这个。以下是一些想法:

侵略状态

跑到距离最近的敌人面前攻击它,直到对方活亡。然后再跑到另一个最接近的敌人旁边,再攻击它至活。这整个过程一直循环到它找不到任何敌人。

听起来很复杂,但实际上,我只是让单位对它看到的任何敌人使用ActionMelee,直到它在它的范围内再也找不到敌人。

防守状态

打击任何攻击它的敌人,但如果敌人撤退或离开它的攻击范围,它便不会再追赶。我还要为此做一个Unit.OnBeingAttacked(Unit attacker)。

警戒状态

(只适用于远程单位)尽可能与敌人保持能够进攻敌人的最远距离。这意味着当它距离目标太近时,它将往后移动;当它距离目标太远时,它将往前移动。

strat souls

反思

也许我应该把中级和最高级合并成一个级。我可能本应试写出代码的,这样我就可以使用我的行为树插件来执行这种高级行为了,但当然,我只有7天时间,时间用花了,所以把这些行为硬编码成那些Action类还是有好处的。

事实上我还少了一些比较基本的东西,如远程攻击、单位队形、寻径、特殊技能(如魔法)等。这些取决于我希望《Strat Souls》达到的设计程度吧。

我还没给游戏添加建筑,我想我可能得再次修改代码。

我还应该做一个分开的UnitAttack类作为最低级类的一部分。它可以处理近战碰撞和启动投射动作。不过,我没有时间执行合适的运程攻击,所以我只能做到那样了。

各个级都为上一级提供便利的功能。

这么做有两个好处:

最小化修改的影响:修改一个级的代码时不必大改其他级的代码(有时候甚至完全不必修改),最小化重写工作、人为错误和漏洞。因为游戏的开发本来就是一个重复制作的过程(你要不断改进原型),所以反复修改代码是不可避免的。

例如,如果你打算切换到CharacterController而不是Rigidbody,会怎么样呢?你知道必须修改的唯一一个类是UnitMovement类;它是唯一一个处理物理系统的类。所以你不必担心你的其他类是否需要修改。如果你想切换到Photon networking library,又怎么样呢?你只要修改UnitNetwork类。

这就使得我可以很轻易地转换出游戏的不同的亚系统。如果你必须要,你可以把你的代码移植到2D游戏引擎,并且只需要大改低级类。

管理复杂度:像这种结构更易于思考,因为你通常一次只需要处理一个级。当你写代码时,比如说巡逻行为,你不必担心刚体、速度或碰撞之类的东西。你只要使用你在Unit类中做的便利功能就行了。

如果你确实需要制作新的便利功能,那么在你修改低级层时,你同样不必担心(太多)高级行为。

总有时候你必须反复修改所有级的代码,特别是当你的系统还不稳定的时候(当你仍然在改进原型时)。但一旦你确定你希望单位如何表现,那么你通常只要一次处理一个级。为大脑减轻了不少负担啊。

给单位的类是不是太多了?

当然要这么多。单位是游戏的关键部分,所以根据单位做出这么庞大的系统是应该的。

但这样会降低游戏速度吧?

通常来说,不会。这个概念本身就不应该影响你的游戏的帧速率,但如果你确实要在紧凑的循环中执行非常复杂的计算,那么当然会有一些延迟。

至于其他的事,在你指点其他东西前先看你的代码表现吧。记住,你的代码是按级分开的。当你的发现阻塞,你可以优化有问题的部分,并不会太影响其他级的代码。

听起来还是很复杂。

你之后会了解为什么这么做是徝得的。

就像我一样。

无论如何,我解释的代码只是让游戏系统的结构清楚的方法之一。也有可能,你的游戏更简单,所以你的执行方法也更简单。

但无论你的执行方法是什么,底层概念应该总是一样的:松耦合&模块化(注:也就是可以轻易地从一个亚系统切换到另一个)、抽象(通过使用便利功能隐藏低级细节)和封闭(不允许你的高级类直接干扰低级类的变量)。

声明:我其实没有接受过软件工程的正规教育,我还在学习中。所有知识都是通过网络、朋友讨论和看书学来的。