游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

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

发布时间:2013-08-15 13:47:12 Tags:,,,

作者:Ferdinand Joseph Fernandez

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

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

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

Strat Souls(from gamasutra)

Strat Souls(from gamasutra)

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

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

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

最低级类

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

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

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

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

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

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

……

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

strat souls 2(from gamasutra)

strat souls (from gamasutra)

基本的问题域级类

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

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

但“问题域”到底是什么?因为我的游戏是一款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 3(from gamasutra)

strat souls (from gamasutra)

中级类

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

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

所以单体可以做不同类型的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(from gamasutra)

strat souls(from gamasutra)

反思

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

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

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

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

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

这么做有两个好处:

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

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

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

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

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

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

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

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

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

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

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

听起来还是很复杂。

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

就像我一样。

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

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

声明:我其实没有接受过软件工程的正规教育,我还在学习中。所有知识都是通过网络、朋友讨论和看书学来的。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Using Software Engineering to Streamline a 7-day RTS

by Ferdinand Joseph Fernandez

My entry to the #7dRTS is called Strat Souls, a simple multiplayer skirmish RTS game.

So how do you manage creating something as complex as a real-time strategy game? Especially if you’re a single programmer doing the whole thing with only seven days?

I did mine with standard software engineering principles.

The idea is to segregate your classes from low-level details to high-level concepts.

I’d usually explain this by terminologies like encapsulation, abstraction, loose coupling, etc. but I’ll explain by example.

Here are the set of classes just for a unit. You’ll see there are quite a lot of them.

Lowest Level

These classes deal directly with the Unity system (the game engine that I use). One class does one thing only.

Generally, no other classes should directly call Unity’s innards except these (but there are exceptions).

UnitMovement class: Deals with moving and rotating the unit. The only class that directly controls the unit’s rigidbody. Also handles movement collisions and obstacle avoidance.

UnitAnimation class: The only class that controls Mecanim (via the Animator class). Mecanim is Unity’s animation system.

UnitNetwork class: The only class that deals with NetworkView and Network. Sends and receives network data for the unit. For the unit, this is the only class that has RPC’s and network state
syncs.

Health class: Manages this unit’s health, very basic piece of code.

etc.

The idea is that these classes are the bridge between Unity’s systems and my code. That means my code more or less doesn’t directly access Unity classes (except the aforementioned ones).

Basic Problem-Domain Level

Note: the names of these levels are just something I made up

On top of the Lowest Level is the Basic Problem-Domain Level. It makes use of the classes of the Lowest Level (specifically, via the public functions they provide) to create higher-level convenience functions. All levels above this continue to deal in terms of the problem domain.

Ok, but what the hell does problem-domain mean? It means since my game is an RTS, my functions are now structured in terms of TurnToTargetUnit(), instead of RotateToVector().

I now think in terms of an RTS game, not as a basic 3d system. That’s because my problem-domain is real-time strategy. So my code deals in terms of real-time strategy concepts. It provide functions where the caller doesn’t need to know the underlying 3d world and physics system, or at least, lessen the need to know that.

This level contains only one class, the Unit, which acts as the fa?ade class to the various Lowest Level classes. There is no inheritance here. Unit simply has variables to the existing UnitMovement, UnitAnimation, UnitNetwork, etc.

The Unit class has public functions such as:

MoveToPosition or MoveToUnit: uses UnitMovement to actually move, and UnitAnimation to play the move animation for you

DoMeleeAttack: plays melee attack animation (via UnitAnimation), and turns on melee collisions. It also ensures this action is replicated across the network when in a multiplayer game (via
UnitNetwork)

IsNearPosition or IsNearUnit: uses UnitMovement to check if this unit is near enough a position or another unit

IsFacingUnit: uses UnitMovement to check if this unit is facing another unit

IsNearAndFacingUnit: convenience function that combines the two

IsDead/IsAlive: uses Health class to check if unit is alive or dead

GetAllEnemiesNearMe: returns an array of enemy units that are within specified radius, sorted by proximity etc.

As you can see, Unit merely defers the actual work to the Lowest Level classes. Unit is like the conductor in an orchestra, he doesn’t really play any instruments, but he coordinates the ones who do. He tells them what to do, and at what time.

Medium Level

Adds basic behaviour on top of the Basic Problem-Domain Level.

The Medium Level now directly corresponds to what actions a unit does when you right-click on something. Since I want different unit types to do different things, I separated this per class.

So I have different types of Actions that the unit can do. I created an abstract base class Action, and the Unit class has two variables of these, one it uses when you right-click the ground, and
one it uses when you right-click an enemy.

Then I have different subclasses of Action:

ActionMove

Will simply move to the requested destination (using Unit.MoveToPosition).

Take note that the Unit class handles both the physics of moving, and playing the move animation for us with that single function call.

ActionMelee

Will move near enough the targeted enemy (calls Unit.MoveToUnit), and once it reaches it (continually checking the return value of Unit.IsNearAndFacingUnit), will keep attacking it (calls

Unit.DoMeleeAttack) until the target dies (checks return value of Unit.IsDead)

Again, take note that calling all those functions ensure that the animations are played properly for us, and that movement and attacks are synced across the network without us needing to worry about it, because the Lowest Level handles that for us. At this level, we only care about designing the behaviour of our unit, not low-level details like setting the speed, or blending between two animations.

There would be other things, like ActionRangedAttack, or ActionDashAttack (the one that Bone Wheel uses in my game), perhaps an ActionBuffAlly, etc.

So now I provide higher-level behaviours by combining the use of public functions that the Unit class provides. The code in my Action classes is like a MacGyver that combines different low-level tools to finally do useful stuff.

Also, I can assign different Actions to units. This means I can turn a melee unit into a ranged unit by swapping what Action it uses.

(How those Action classes can get what the requested destination or target is, is handled by my Order singleton class. It simply gets the 3d x,y,z position of where the mouse cursor is, or the actual Unit that is under the mouse, if there is any. It communicates with myUnitSelector singleton to tell all currently selected units that a command was issued (when a right-click is detected).

But what each unit exactly does when such command is issued, is determined by which Action subclass it uses.)

Highest Level

Adds behaviours that players normally expect. I actually did not have time to implement this for the #7dRTS. Here are some ideas:

Aggressive stance

Go to the nearest enemy it can find, attack until it dies. Then go to the next nearest enemy again, and kill it. Repeat the whole process, until it can’t find any more enemies.

Sounds complicated to code, but really, all it does is keep on using ActionMelee on every enemy it sees, until it can’t find any more enemies in its vicinity.

Hold Position stance

Retaliate against any enemy that engages it, but do not pursue if the enemy retreats or goes out of range of my attacks. I would have to create a Unit.OnBeingAttacked(Unit attacker) for this.

Cautious stance

(for ranged units only) Maintain farthest distance possible from any enemy and attack it from afar. This means it will move backwards if it is too close the target, or move forward if it gets too far.

Caveats

Maybe I should have combined my Medium Level and the planned Highest Level into one, actually. I also probably should have coded it so that I can use my behaviour tree plugin to implement such high-level behaviours, but of course, I only had 7 days for this, and time is running out, so there is also merit to hard-coding the behaviours into those Action classes in the meantime.

There are actually a few more fundamental things lacking here, like ranged attacks, unit formations, pathfinding, special abilities (i.e. spells), and perhaps more depending on what design I want Strat Souls to move towards.

I also have not added structures/buildings in the game yet, and I imagine I may need to change the code once more to accommodate that.

I should also have made a separate UnitAttack class as part of the Lowest Level. It would deal with melee collisions and launching of projectiles. I did not have time to implement proper ranged attacks though, so I left it at that.

So, that’s it

The idea is each level provides convenience functions to the one above it.

There are two benefits to this:

Minimize the impact of change: Changing code in one level, shields the other levels from needing to be changed too much (sometimes, not at all), minimizing re-writes, human mistakes, and bugs. And with the nature of game development being iterative (you keep on refining the prototype), you will deal with having to change code constantly.

For example, what if you decided you want to switch to CharacterController instead of Rigidbody? You’d know that the only class that needs to be changed is the UnitMovement class; it’s the only one that handles the physics system. So you don’t need to worry if your other classes need to be changed. What if you want to switch to the Photon networking library? You’d only change the UnitNetwork class. And so on.

This makes it easy to swap out different sub-systems for your game. Heck, if you really need to, you can port your code to a 2d game engine and only the low-level classes need to have drastic changes.

Manages complexity: Structures like this allow you to think easier because you usually only need to deal with one level at a time. When you’re coding, say, a patrol behaviour, you don’t need to worry about rigidbody.velocity or OnCollisionEnter or anything like that. You just use the convenience functions that you made in the Unit class.

In case you need to create new convenience functions, then likewise you don’t need to worry (too much) about higher-level behaviours while you deal with changing the low-level ones.

There will be times where you need to zip back-and-forth changing code in all levels, most likely when your system isn’t stable yet (when you’re still iterating on ideas). But once you’re definite with how you want things to work, you’d usually only deal with one level at a time. And that is a huge load off your brain.

Aren’t these too much classes just for your game’s units?

Why not? Units are the central feature of the game, so it makes sense to devote a big system around it.

But doesn’t all this make my game slow???

Generally, no. The concept itself shouldn’t impact your game’s framerate, but if you do your particular implementation where, for example, you’re doing an expensive calculation in a tight loop, then obviously you’ll see some slowness.

As with anything, measure your code’s performance first, before you start pointing fingers anywhere. And even then, remember, you structured your code where things are segregated by levels. When you find the bottleneck, you can optimize that problematic part without impacting the other levels of your code too much.

This all sounds too complicated and will just get in my way!

Oh, you’ll learn to see why this is worth it.

Oh yes.

Just like I did.

Anyone care to share any programmer horror stories?

In any case, the code I explained is only one way of achieving a clean structure for your game’s system. It could be that your design for your game is simpler, and you’d arrive with a far more
simple implementation.

But whichever particular implementation you make, the underlying concepts should always stay the same: loose-coupling & modularity (the idea that you can easily swap one sub-system for another), abstraction (hiding low-level details by the use of convenience functions), and encapsulation (disallowing your high-level classes from directly messing with the variables of low-level classes).

Disclaimer: I actually have no formal education on software engineering, and I’m still learning. I got all that I know from the Internet, discussions with friends, and really good books. I recommend Code Complete.(source:gamasutra)


上一篇:

下一篇: