游戏中几乎总是有一个玩家等级。玩家通常可以在游戏中做很多事情,这对我而言,这堂课最终变得庞大,需要大量变量来支持玩家可以执行的每项功能。每个部分都很小,但是结合起来,我最终要编写成千上万行代码,找到您需要的内容会很痛苦,而进行更改会让人感到恐惧。基本上是整个游戏的通用控件,如何避免这个问题?
游戏中几乎总是有一个玩家等级。玩家通常可以在游戏中做很多事情,这对我而言,这堂课最终变得庞大,需要大量变量来支持玩家可以执行的每项功能。每个部分都很小,但是结合起来,我最终要编写成千上万行代码,找到您需要的内容会很痛苦,而进行更改会让人感到恐惧。基本上是整个游戏的通用控件,如何避免这个问题?
Answers:
您通常会使用实体组件系统(实体组件系统是基于组件的体系结构)。这也使创建其他实体的方式更加容易,并且还可以使敌人/ NPC与玩家具有相同的组件。
这种方法与面向对象的方法完全相反。游戏中的一切都是实体。实体只是一个案例,没有内置任何游戏机制。它具有组件列表以及操作它们的方法。
例如,播放器具有位置组件,动画组件和输入组件,并且当用户按下空间时,您希望播放器跳起来。
您可以通过为播放器实体提供一个跳跃组件来实现此目的,当调用该实体时,动画组件会变为跳跃动画,并使播放器在位置组件中具有正y速度。在输入组件中,您侦听空格键,然后调用跳转组件。(这只是一个示例,您应该具有用于移动的控制器组件)。
这有助于将代码分解为较小的可重用模块,并可以使项目更有条理。
游戏不是唯一的。神级到处都是反模式。
常见的解决方案是将大型类别分解为较小类别的树。如果播放器有库存,请不要将库存管理作为的一部分class Player
。而是创建一个class Inventory
。这是的成员class Player
,但是内部class Inventory
可以包装许多代码。
另一个例子:玩家角色可能与NPC有关系,因此您可能class Relation
同时引用Player
对象和NPC
对象,但两者都不属于。
1)播放器:状态机+基于组件的体系结构。
播放器的常用组件:HealthSystem,MovementSystem,InventorySystem,ActionSystem。这些都是像的类class HealthSystem
。
我不建议使用Update()
有(这使得在通常情况下是没有意义的有卫生系统更新,除非你需要它的一些行动有每一帧,这些很少出现的一个情况下,你也可以想到的-玩家被下毒,你需要他有时会失去健康-我在这里建议使用协程;另一种会不断再生健康或运行能力,您只需掌握当前的运行状况或能量,并在时间到时调用协程以补充该水平。他被损坏了,或者他又开始跑了,依此类推。好吧,这有点离题,但我希望它很有用。
状态:LootState,RunState,WalkState,AttackState,IDLEState。
每个状态都继承自interface IState
。IState
在我们的案例中,仅以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
-我们知道,我们必须切换_currentPlayerState
到RunState
,我们在这样做Run()
的WalkState
,并用不同的状态下,再次调用它里面的方法,但现在,因为我们不想失去这个行动框架。现在我们当然打电话给_playerMovementSystem.Run();
。
但是,LootState
如果玩家在释放按钮之前不能走路或跑步,该怎么办?好了,在这种情况下,当我们开始掠夺时,例如E
按下按钮时,我们调用_currentPlayerState.Loot();
我们切换到LootState
现在从那里调用它的调用。例如,我们在那里调用collsion方法来获取范围内是否有东西需要抢劫。我们在有动画或在哪里开始动画的地方调用协程,并检查玩家是否仍然按住按钮,如果协程没有中断,如果是,我们在协程结束时给予他战利品。但是如果玩家按下该WASD
怎么办?- _currentPlayerState.Walk();
被称为,但是这是关于状态机的漂亮之处,LootState.Walk()
我们有一个空的方法,它什么也不做,或者像我将做的那样-玩家说:“嘿,我还没有洗劫,您可以等吗?” 当他结束抢劫时,我们转到IDLEState
。
另外,您可以执行另一个脚本,该脚本class BaseState : IState
实现了所有这些默认方法的行为,但是有这些脚本,virtual
因此您可以override
按class LootState : BaseState
类的类型进行操作。
基于组件的系统很棒,唯一让我困扰的是实例,其中很多。而且它需要更多的内存并用于垃圾收集器。例如,如果您有1000个敌人实例。它们全部具有4个组件。4000个对象而不是1000个对象。如果考虑统一游戏对象具有的所有组件,那么Mb并不是什么大问题(我还没有运行性能测试)。
2)基于继承的体系结构。尽管您会注意到我们无法完全摆脱组件-如果我们想要干净且有效的代码,这实际上是不可能的。另外,如果我们要使用强烈建议在适当情况下使用的设计模式(也不要过度使用它们,则称为过度设计)。
想象一下,我们有一个Player类,该类具有退出游戏所需的所有属性。它具有健康,法力或能量,具有移动,奔跑和使用的能力,具有存货,可以制作物品,战利品,甚至可以建造路障或炮塔。
首先,我要说的是,库存,制作,移动,构建应该基于组件,因为拥有这样的方法不是玩家的责任AddItemToInventoryArray()
-尽管玩家可以拥有一个这样的方法PutItemToInventory()
来调用前面描述的方法(2层-我们可以根据不同的图层添加一些条件)。
建筑的另一个例子。播放器可以调用诸如之类的东西OpenBuildingWindow()
,但Building
会处理其余所有工作,并且当用户决定建造某些特定建筑物时,他会将所有所需的信息传递给播放器,Build(BuildingInfo someBuildingInfo)
然后播放器便开始使用所需的所有动画来构建它。
SOLID-OOP原则。S-单一责任:这是我们在先前示例中所看到的。是的,但是继承在哪里?
此处:玩家的健康状况和其他特征是否应由其他实体处理?我想不是。没有健康就不会有玩家,如果有健康,我们就不会继承。例如,我们有IDamagable
,LivingEntity
,IGameActor
,GameActor
。IDamagable
当然有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 : Building
,TechBuilding : Building
。您无需创建2个组件并在其中编写两次代码即可进行通用操作或构建属性。然后以不同的方式添加它们,您可以使用继承的功能,以后可以使用多态性和封装。
我建议在两者之间使用一些东西。并且不要过度使用组件。
我强烈建议您阅读有关游戏编程模式的书-在Web上免费。
这个问题没有灵丹妙药,但是有各种各样的方法,几乎所有方法都围绕着“关注点分离”的原则。其他答案已经讨论了流行的基于组件的方法,但是还有其他方法可以代替基于组件的解决方案或与之一起使用。我将讨论实体控制器方法,因为它是该问题的首选解决方案之一。
首先,关于Player
班级的想法首先是令人误解的。许多人倾向于认为玩家角色,npc角色和怪物/敌人是不同的类,而实际上所有这些都有很多共同点:它们全部绘制在屏幕上,它们都可以移动,它们可能都有库存等
这种思维方式导致玩家角色,非玩家角色和怪物/敌人都被视为“ Entity
”,而不是被不同地对待。当然,它们的行为必须有所不同-玩家角色必须通过输入进行控制,而NPC需要AI。
解决方案是拥有Controller
用于控制Entity
s的类。通过这样做,所有繁重的逻辑最终都将出现在控制器中,并且所有数据和通用性都存储在实体中。
此外,通过Controller
将InputController
和分为子类AIController
,它允许播放器有效控制Entity
房间中的任何内容。这种方法还可以通过使RemoteController
或NetworkController
类通过网络流中的命令进行操作来帮助多人游戏。
Controller
如果您不小心的话,这可能会导致很多逻辑陷入困境。避免这种情况的方法是使Controller
s由其他Controller
s 组成,或者使Controller
功能取决于的各种属性Controller
。例如,AIController
将具有DecisionTree
附着到它,并且PlayerCharacterController
可以由不同的其他Controller
S,从而为MovementController
,一个JumpController
(包含状态机与所述状态OnGround,升序和降序),一个InventoryUIController
。这样做的另一个好处是,Controller
可以在添加新功能时添加新s-如果游戏在没有库存系统的情况下开始并且添加了一个游戏,则可以在以后添加该游戏的控制器。