如何避免巨人课程?


46

游戏中几乎总是有一个玩家等级。玩家通常可以在游戏中做很多事情,这对我而言,这堂课最终变得庞大,需要大量变量来支持玩家可以执行的每项功能。每个部分都很小,但是结合起来,我最终要编写成千上万行代码,找到您需要的内容会很痛苦,而进行更改会让人感到恐惧。基本上是整个游戏的通用控件,如何避免这个问题?


26
多个文件或一个文件,代码必须放在某个地方。游戏很复杂。要找到所需的内容,请写出好的方法名称和描述性注释。不要害怕进行更改-只是测试。并备份您的工作:)
克里斯·麦克法兰

7
我知道它必须去某个地方,但是代码设计在灵活性和维护方面很重要。拥有成千上万行的类或代码组也不会让我感到惊讶。
user441521 '17

17
@ChrisMcFarland不建议备份,建议使用版本代码XD。
GameDeveloper

1
@ChrisMcFarland我同意GameDeveloper。拥有Git,svn,TFS等版本控制功能,因为能够更轻松地撤消重大更改,并且能够轻松地从意外删除项目,硬件故障或文件损坏等故障中恢复过来,因此开发工作变得非常容易。
Nzall

3
@TylerH:我非常不同意。备份不允许将许多探索性更改合并在一起,也不允许将尽可能多的有用元数据绑定到变更集,也不能启用合理的多开发人员工作流。您可以像非常强大的时间点备份系统一样使用版本控制,但是这些工具缺少很多潜力。
Phoshi

Answers:


67

您通常会使用实体组件系统(实体组件系统是基于组件的体系结构)。这也使创建其他实体的方式更加容易,并且还可以使敌人/ NPC与玩家具有相同的组件。

这种方法与面向对象的方法完全相反。游戏中的一切都是实体。实体只是一个案例,没有内置任何游戏机制。它具有组件列表以及操作它们的方法。

例如,播放器具有位置组件,动画组件和输入组件,并且当用户按下空间时,您希望播放器跳起来。

您可以通过为播放器实体提供一个跳跃组件来实现此目的,当调用该实体时,动画组件会变为跳跃动画,并使播放器在位置组件中具有正y速度。在输入组件中,您侦听空格键,然后调用跳转组件。(这只是一个示例,您应该具有用于​​移动的控制器组件)。

这有助于将代码分解为较小的可重用模块,并可以使项目更有条理。


评论不作进一步讨论;此对话已转移至聊天
MichaelHouse

8
虽然我理解需要移动的注释,但是不要移动那些挑战答案准确性的注释。那应该很明显,不是吗?
bug-a-lot

20

游戏不是唯一的。神级到处都是反模式。

常见的解决方案是将大型类别分解为较小类别的树。如果播放器有库存,请不要将库存管理作为的一部分class Player。而是创建一个class Inventory。这是的成员class Player,但是内部class Inventory可以包装许多代码。

另一个例子:玩家角色可能与NPC有关系,因此您可能class Relation同时引用Player对象和NPC对象,但两者都不属于。


是的,我只是在寻找有关如何执行此操作的想法。心态是因为有很多小功能,所以在编码时,对我来说,分解那些小功能并不自然。但是,显而易见的是,所有这些小功能开始使播放器类变得庞大。
user441521 '17

1
人们通常说某物是神类或神物,当它包含并管理游戏中的所有其他类/物时。
巴林特

11

1)播放器:状态机+基于组件的体系结构。

播放器的常用组件:HealthSystem,MovementSystem,InventorySystem,ActionSystem。这些都是像的类class HealthSystem

我不建议使用Update()(这使得在通常情况下是没有意义的有卫生系统更新,除非你需要它的一些行动有每一帧,这些很少出现的一个情况下,你也可以想到的-玩家被下毒,你需要他有时会失去健康-我在这里建议使用协程;另一种会不断再生健康或运行能力,您只需掌握当前的运行状况或能量,并在时间到时调用协程以补充该水平。他被损坏了,或者他又开始跑了,依此类推。好吧,这有点离题,但我希望它很有用

状态:LootState,RunState,WalkState,AttackState,IDLEState。

每个状态都继承自interface IStateIState在我们的案例中,仅以4种方法为例。Loot() Run() Walk() Attack()

另外,我们在class InputController哪里检查用户的每个输入。

现在InputController来看一个真实的例子:在我们中,检查玩家是否按下了WASD or arrows,然后是否也按下Shift。如果他只按了,WASD那么我们会_currentPlayerState.Walk();在这种情况发生时打电话给我们,并且currentPlayerState必须等于,WalkState然后WalkState.Walk() 我们拥有该状态所需的所有组件-在这种情况下MovementSystem,让玩家移动public void Walk() { _playerMovementSystem.Walk(); }-您看到我们这里有什么吗?我们有第二层行为,这对于代码维护和调试非常有用。

现在转到第二种情况:如果我们按WASD+会Shift怎样?但是我们以前的状态是WalkState。在这种情况下Run()将被调用InputController(不要混淆,Run()之所以调用是因为我们有WASD+ Shift签入InputController不是因为WalkState)。当我们调用_currentPlayerState.Run();WalkState-我们知道,我们必须切换_currentPlayerStateRunState,我们在这样做Run()WalkState,并用不同的状态下,再次调用它里面的方法,但现在,因为我们不想失去这个行动框架。现在我们当然打电话给_playerMovementSystem.Run();

但是,LootState如果玩家在释放按钮之前不能走路或跑步,该怎么办?好了,在这种情况下,当我们开始掠夺时,例如E按下按钮时,我们调用_currentPlayerState.Loot();我们切换到LootState现在从那里调用它的调用。例如,我们在那里调用collsion方法来获取范围内是否有东西需要抢劫。我们在有动画或在哪里开始动画的地方调用协程,并检查玩家是否仍然按住按钮,如果协程没有中断,如果是,我们在协程结束时给予他战利品。但是如果玩家按下该WASD怎么办?- _currentPlayerState.Walk();被称为,但是这是关于状态机的漂亮之处,LootState.Walk()我们有一个空的方法,它什么也不做,或者像我将做的那样-玩家说:“嘿,我还没有洗劫,您可以等吗?” 当他结束抢劫时,我们转到IDLEState

另外,您可以执行另一个脚本,该脚本class BaseState : IState实现了所有这些默认方法的行为,但是有这些脚本,virtual因此您可以overrideclass LootState : BaseState类的类型进行操作。


基于组件的系统很棒,唯一让我困扰的是实例,其中很多。而且它需要更多的内存并用于垃圾收集器。例如,如果您有1000个敌人实例。它们全部具有4个组件。4000个对象而不是1000个对象。如果考虑统一游戏对象具有的所有组件,那么Mb并不是什么大问题(我还没有运行性能测试)。


2)基于继承的体系结构。尽管您会注意到我们无法完全摆脱组件-如果我们想要干净且有效的代码,这实际上是不可能的。另外,如果我们要使用强烈建议在适当情况下使用的设计模式(也不要过度使用它们,则称为过度设计)。

想象一下,我们有一个Player类,该类具有退出游戏所需的所有属性。它具有健康,法力或能量,具有移动,奔跑和使用的能力,具有存货,可以制作物品,战利品,甚至可以建造路障或炮塔。

首先,我要说的是,库存,制作,移动,构建应该基于组件,因为拥有这样的方法不是玩家的责任AddItemToInventoryArray()-尽管玩家可以拥有一个这样的方法PutItemToInventory()来调用前面描述的方法(2层-我们可以根据不同的图层添加一些条件)。

建筑的另一个例子。播放器可以调用诸如之类的东西OpenBuildingWindow(),但Building会处理其余所有工作,并且当用户决定建造某些特定建筑物时,他会将所有所需的信息传递给播放器,Build(BuildingInfo someBuildingInfo)然后播放器便开始使用所需的所有动画来构建它。

SOLID-OOP原则。S-单一责任:这是我们在先前示例中所看到的。是的,但是继承在哪里?

此处:玩家的健康状况和其他特征是否应由其他实体处理?我想不是。没有健康就不会有玩家,如果有健康,我们就不会继承。例如,我们有IDamagableLivingEntityIGameActorGameActorIDamagable当然有TakeDamage()

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

因此,在这里我实际上无法从继承中划分组件,但是我们可以像您看到的那样混合它们。例如,如果我们有一些不同的类型并且我们不想编写超出需要的代码,我们还可以为Building system创建一些基类。确实,我们也可以拥有不同类型的建筑物,实际上没有基于组件的好方法!

OrganicBuilding : BuildingTechBuilding : Building。您无需创建2个组件并在其中编写两次代码即可进行通用操作或构建属性。然后以不同的方式添加它们,您可以使用继承的功能,以后可以使用多态性和封装。


我建议在两者之间使用一些东西。并且不要过度使用组件。


我强烈建议您阅读有关游戏编程模式的书-在Web上免费。


我将在今晚晚些时候进行挖掘,但是仅供参考,我没有使用统一性,所以我必须调整一些。
user441521 '17

哦,对不起,我认为这是一个Unity标签,不好。唯一的是MonoBehavior-它只是Unity编辑器中场景中每个实例的基类。至于Physics.OverlapSphere()-这是一种在框架期间创建球体碰撞器并检查其触摸方式的方法。协程就像伪造的Update,可以将它们的呼叫减少到比播放器PC上的fps少的数量-有利于性能。Start()-创建实例时仅调用一次的方法。其他所有内容都应适用于其他任何地方。下一部分,我不会在Unity中使用任何东西。对不起 希望这可以澄清一些事情。
坦率的月亮_Max_

我之前使用过Unity,所以我了解这个想法。我正在使用具有协程的Lua,所以事情应该翻译的很好。
user441521 '17

考虑到缺少Unity标签,这个答案似乎过于特定于Unity。如果您使它更通用并且使统一性更多地成为示例,那么这将是一个更好的答案。
法拉普

@CandidMoon是的,那更好。
法拉普

4

这个问题没有灵丹妙药,但是有各种各样的方法,几乎​​所有方法都围绕着“关注点分离”的原则。其他答案已经讨论了流行的基于组件的方法,但是还有其他方法可以代替基于组件的解决方案或与之一起使用。我将讨论实体控制器方法,因为它是该问题的首选解决方案之一。

首先,关于Player班级的想法首先是令人误解的。许多人倾向于认为玩家角色,npc角色和怪物/敌人是不同的类,而实际上所有这些都有很多共同点:它们全部绘制在屏幕上,它们都可以移动,它们可能都有库存等

这种思维方式导致玩家角色,非玩家角色和怪物/敌人都被视为“ Entity”,而不是被不同地对待。当然,它们的行为必须有所不同-玩家角色必须通过输入进行控制,而NPC需要AI。

解决方案是拥有Controller用于控制Entitys的类。通过这样做,所有繁重的逻辑最终都将出现在控制器中,并且所有数据和通用性都存储在实体中。

此外,通过ControllerInputController和分为子类AIController,它允许播放器有效控制Entity房间中的任何内容。这种方法还可以通过使RemoteControllerNetworkController类通过网络流中的命令进行操作来帮助多人游戏。

Controller如果您不小心的话,这可能会导致很多逻辑陷入困境。避免这种情况的方法是使Controllers由其他Controllers 组成,或者使Controller功能取决于的各种属性Controller。例如,AIController将具有DecisionTree附着到它,并且PlayerCharacterController可以由不同的其他ControllerS,从而为MovementController,一个JumpController(包含状态机与所述状态OnGround,升序和降序),一个InventoryUIController。这样做的另一个好处是,Controller可以在添加新功能时添加新s-如果游戏在没有库存系统的情况下开始并且添加了一个游戏,则可以在以后添加该游戏的控制器。


我喜欢这个主意,但似乎已经将所有代码都转移到了控制器类,这给我带来了同样的问题。
user441521 '17

@ user441521刚刚意识到我要添加一个额外的段落,但是当我的浏览器崩溃时我丢失了它。我现在将其添加。基本上,您可以让不同的控制器将它们组成聚合控制器,以便每个控制器处理不同的事情。例如,AggregateController.Controllers = {JumpController(keybinds),MoveController(keybinds),InventoryUIController(keybinds,uisystem)}
Pharap
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.