题
作为一个小型练习,我正在尝试编写一个非常简单的游戏引擎,该引擎只能处理实体(移动,基本AI等)
因此,我正在尝试考虑游戏如何处理所有实体的更新,而且我感到有些困惑(可能是因为我以错误的方式进行操作)
因此,我决定在此处发布此问题,以向您展示我目前的思考方式,看看是否有人可以向我建议这样做的更好的方法。
目前,我有一个Cengine课程,可以将指针带到所需的其他类(例如Cwindow类,CentityManager类等)
我有一个游戏循环,在伪代码中会像这样(在Cengine类中)
while(isRunning) {
Window->clear_screen();
EntityManager->draw();
Window->flip_screen();
// Cap FPS
}
我的中心曼班的课看起来像这样:
enum {
PLAYER,
ENEMY,
ALLY
};
class CEntityManager {
public:
void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc.
void delete_entity(int entityID);
private:
std::vector<CEntity*> entityVector;
std::vector<CEntity*> entityVectorIter;
};
我的中心课看起来像这样:
class CEntity() {
public:
virtual void draw() = 0;
void set_id(int nextEntityID);
int get_id();
int get_type();
private:
static nextEntityID;
int entityID;
int entityType;
};
之后,我将创建类,例如,为敌人,给它一个精灵表,其自身的功能等。
例如:
class CEnemy : public CEntity {
public:
void draw(); // Implement draw();
void do_ai_stuff();
};
class CPlayer : public CEntity {
public:
void draw(); // Implement draw();
void handle_input();
};
所有这些都可以在屏幕上绘制精灵绘制精灵。
但是后来我遇到了使用一个实体中存在但不存在另一个实体的功能的问题。
在上面的伪代码示例中,do_ai_stuff();和hander_input();
从我的游戏循环中可以看到,有一个呼叫EntityManager-> draw();这只是通过实体向量迭代并称为draw();每个实体的功能 - 所有实体都有绘制();功能。
但是我想,如果需要处理输入的玩家实体怎么办?这是如何运作的?
我没有尝试过,但是我认为我不能像对draw()函数一样循环,因为像敌人这样的实体没有handle_input()函数。
我可以使用if语句检查实体tytype,例如:
for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) {
if((*entityVectorIter)->get_type() == PLAYER) {
(*entityVectorIter)->handle_input();
}
}
但是我不知道人们通常如何写这些东西,所以我不确定最好的方法。
我在这里写了很多东西,我没有问任何具体问题,所以我会澄清我在这里寻找的内容:
- 我布置/设计我的代码的方式是否可以,这是实用的吗?
- 我是否有更好的方法来更新我的实体并调用其他实体可能没有的功能?
- 是否使用枚举来跟踪实体类型的一种识别实体的好方法?
解决方案
您已经非常接近大多数游戏的实际情况(尽管表现专家Curmudgeon Mike Acton 经常抓住这一点).
通常你会看到这样的东西
class CEntity {
public:
virtual void draw() {}; // default implementations do nothing
virtual void update() {} ;
virtual void handleinput( const inputdata &input ) {};
}
class CEnemy : public CEntity {
public:
virtual void draw(); // implemented...
virtual void update() { do_ai_stuff(); }
// use the default null impl of handleinput because enemies don't care...
}
class CPlayer : public CEntity {
public:
virtual void draw();
virtual void update();
virtual void handleinput( const inputdata &input) {}; // handle input here
}
然后,Entity Manager通过世界上每个实体上的thangeinput()和draw()调用update(),handleinput()和draw()。
当然,拥有很多这些功能,大多数功能在您打电话给它们时都无能为力,尤其是对于虚拟功能而言。所以我也看到了其他一些方法。
一个是存放 例如 全局中的输入数据(或作为全局界面或单件界面等)中的输入数据。然后覆盖敌人的update()函数,因此它们do_ai_stuff()。以及播放器的更新(),以便通过对全球进行轮询来完成输入处理。
另一个是在 听众模式, ,使所有关心输入的所有内容都从普通侦听器类继承,并且您可以向InputManager注册所有这些听众。然后,输入管理器依次调用每个侦听器:
class CInputManager
{
AddListener( IInputListener *pListener );
RemoveListener( IInputListener *pListener );
vector<IInputListener *>m_listeners;
void PerFrame( inputdata *input )
{
for ( i = 0 ; i < m_listeners.count() ; ++i )
{
m_listeners[i]->handleinput(input);
}
}
};
CInputManager g_InputManager; // or a singleton, etc
class IInputListener
{
virtual void handleinput( inputdata *input ) = 0;
IInputListener() { g_InputManager.AddListener(this); }
~IInputListener() { g_InputManager.RemoveListener(this); }
}
class CPlayer : public IInputListener
{
virtual void handleinput( inputdata *input ); // implement this..
}
而且还有其他更复杂的方法。但是所有这些工作,我都在实际运送和出售的东西中看到了它们。
其他提示
您应该考虑组件,而不是为此继承。例如,在我的引擎中,我有(简化):
class GameObject
{
private:
std::map<int, GameComponent*> m_Components;
}; // eo class GameObject
我有各种各样做不同事情的组件:
class GameComponent
{
}; // eo class GameComponent
class LightComponent : public GameComponent // represents a light
class CameraComponent : public GameComponent // represents a camera
class SceneNodeComponent : public GameComponent // represents a scene node
class MeshComponent : public GameComponent // represents a mesh and material
class SoundComponent : public GameComponent // can emit sound
class PhysicsComponent : public GameComponent // applies physics
class ScriptComponent : public GameComponent // allows scripting
这些组件可以添加到游戏对象中以诱导行为。他们可以通过消息传递系统进行通信,以及在主循环注册框架侦听器中需要更新的内容。它们可以独立行动,并在运行时安全地添加/删除。我发现这是一个非常可扩展的系统。
编辑:很抱歉,我会有点充实,但是我现在处于某事的中间:)
您也可以通过使用虚拟函数来意识到此功能:
class CEntity() {
public:
virtual void do_stuff() = 0;
virtual void draw() = 0;
// ...
};
class CEnemy : public CEntity {
public:
void do_stuff() { do_ai_stuff(); }
void draw(); // Implement draw();
void do_ai_stuff();
};
class CPlayer : public CEntity {
public:
void do_stuff() { handle_input(); }
void draw(); // Implement draw();
void handle_input();
};
1 一件小事 - 为什么要更改实体的ID?通常,这是在施工过程中恒定和初始化的,仅此而已:
class CEntity
{
const int m_id;
public:
CEntity(int id) : m_id(id) {}
}
对于其他事项,有不同的方法,选择取决于其中有多少个特定类型的功能(以及如何对其进行评估)。
添加到全部
最简单的方法就是将所有方法添加到基本接口,然后在不支持它的类中作为NO-OP实现。这听起来像是不好的建议,但是如果没有适用的方法,则可以接受接受标记的命名,并且您可以假设一组方法不会随着未来的需求而显着增长。
您甚至可能实施一种基本的“发现机制”,例如
class CEntity
{
public:
...
virtual bool CanMove() = 0;
virtual void Move(CPoint target) = 0;
}
不要过分! 以这种方式启动很容易,然后即使它产生了大量的代码,也要坚持下去。可以将其涂成“类型层次结构的故意统计化” - 但最终是JSUT的入侵,使您可以快速解决一些问题,但是当应用程序增长时会很快受伤。
真正的类型发现
使用和 dynamic_cast
, ,您可以安全地从中施放对象 CEntity
到 CFastCat
. 。如果实体实际上是一个 CReallyUnmovableBoulder
, ,结果将是一个无效指针。这样,您可以为其实际类型探测一个对象,并对其做出相应的反应。
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
fastCat->Meow();
如果仅与特定于类型的方法相关的逻辑很少,则该机制效果很好。它是 不是 一个很好的解决方案,如果您最终探究了许多类型的链条,并采取相应的行动:
// -----BAD BAD BAD BAD Code -----
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
fastCat->Meow();
CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ;
if (bigDog != 0)
bigDog->Bark();
CPebble * pebble = dynamic_cast<CPebble *>(entity) ;
if (pebble != 0)
pebble->UhmWhatNoiseDoesAPebbleMake();
这通常意味着您的虚拟方法不会仔细选择。
接口
当特定于类型的功能不是单个方法,而是方法组时,上面可以扩展到接口。他们在C ++方面非常支持它们,但这是可以忍受的。例如,您的对象具有不同的功能:
class IMovable
{
virtual void SetSpeed() = 0;
virtual void SetTarget(CPoint target) = 0;
virtual CPoint GetPosition() = 0;
virtual ~IMovable() {}
}
class IAttacker
{
virtual int GetStrength() = 0;
virtual void Attack(IAttackable * target) = 0;
virtual void SetAnger(int anger) = 0;
virtual ~IAttacker() {}
}
您的不同对象从基类和一个或多个接口继承:
class CHero : public CEntity, public IMovable, public IAttacker
同样,您可以使用Dynamic_cast来探测任何实体上的接口。
这是非常可扩展的,通常是您不确定的最安全方法。它比上述解决方案有点冗长,但是可以很好地应对未来的意外变化。将功能分解为接口是 不是 容易,需要一些经验才能感觉到它。
访客模式
这 访客模式 需要大量键入,但是它允许您在不修改这些类的情况下为类添加功能。
在您的上下文中,这意味着您可以构建实体结构,但要分别实施他们的活动。通常,当您对实体进行非常不同的操作时,您无法自由修改类,或者将功能添加到类中会强烈违反单一责任性基本。
这几乎可以应付所有更改的要求(前提是您的实体本身已经有充分的变化)。
(我只是在链接到它,因为大多数人需要一段时间才能将其缠绕在它上,除非您经历了其他方法的局限性,否则我不建议使用它)
通常,正如其他人指出的那样,您的代码还可以。
要回答您的第三个问题:在您向我们展示的代码中,除了创建外,您不使用枚举类型。似乎还可以(尽管我想知道“ createplayer()但是,一旦您使用的代码是否使用或什至切换来根据类型进行不同的操作,那么您就会违反某些OO原则。然后,您应该使用虚拟方法的力量来确保他们做必须做的事情。如果您必须“查找”某种类型的对象,则可以在创建它时正确存储指向特殊玩家对象的指针。
如果您只需要一个唯一的ID,您还可以考虑用原始指针替换ID。
请将这些提示视为根据您实际需要的提示。