我有点发痒:说我正在制作一个简单的,2D,类似塞尔达的游戏。 当两个对象发生碰撞时,每个对象都应该有一个结果。然而,当主角与某物碰撞时,他的反应完全取决于他碰撞的物体的类型。如果它是一个怪物,他应该反弹,如果它是一堵墙,什么都不应该发生,如果它是一个带有缎带的魔法蓝盒子,他应该愈合,等等(这些只是例子)。
我还应该注意,两件事情都是碰撞的一部分,也就是说,角色和怪物都会发生碰撞事件,而不仅仅是碰撞事件。
你会怎么写这样的代码?我可以想到一些非常不优雅的方法,例如,在全局WorldObject类中使用虚函数来识别属性 - 例如,GetObjectType()函数(返回int,char * s,将对象标识为Monster的任何内容) ,Box或Wall),然后在具有更多属性的类中,比如Monster,可能会有更多的虚函数,比如GetSpecies()。
但是,维护变得很烦人,并导致冲突处理程序中的大型级联切换(或If)语句
MainCharacter::Handler(Object& obj)
{
switch(obj.GetType())
{
case MONSTER:
switch((*(Monster*)&obj)->GetSpecies())
{
case EVILSCARYDOG:
...
...
}
...
}
}
还有使用文件的选项,文件可能包含:
Object=Monster
Species=EvilScaryDog
Subspecies=Boss
然后代码可以检索属性,而不需要虚拟函数混乱一切。但是,这并不能解决级联If问题。
然后可以选择为每个案例设置一个函数,比如CollideWall(),CollideMonster(),CollideHealingThingy()。这是我个人最不喜欢的(尽管它们远非可爱),因为它似乎是最难维护的。
有人可以对这个问题提供更优雅的解决方案吗? 感谢您的帮助!
答案 0 :(得分:11)
我会这样做反之亦然 - 因为如果角色与一个物体碰撞,物体也会与角色碰撞。因此,您可以拥有一个基类Object,如下所示:
class Object {
virtual void collideWithCharacter(MainCharacter&) = 0;
};
class Monster : public Object {
virtual void collideWithCharacter(MainCharacter&) { /* Monster collision handler */ }
};
// etc. for each object
通常在OOP设计中,虚拟函数是这种情况的唯一“正确”解决方案:
switch (obj.getType()) {
case A: /* ... */ break;
case B: /* ... */ break;
}
修改强>:
澄清之后,您需要稍微调整一下。 MainCharacter
应该为它可能碰撞的每个对象重载方法:
class MainCharacter {
void collideWith(Monster&) { /* ... */ }
void collideWith(EvilScaryDog&) { /* ... */ }
void collideWith(Boss&) { /* ... */ }
/* etc. for each object */
};
class Object {
virtual void collideWithCharacter(MainCharacter&) = 0;
};
class Monster : public Object {
virtual void collideWithCharacter(MainCharacter& c)
{
c.collideWith(*this); // Tell the main character it collided with us
/* ... */
}
};
/* So on for each object */
通过这种方式,您可以通知主角关于碰撞,并且可以采取适当的措施。此外,如果您需要一个不应该通知主角的碰撞对象,您只需删除该特定类中的通知调用。
这种方法称为double dispatch。
我还会考虑将MainCharacter本身设为Object
,将重载移至Object
并使用collideWith
代替collideWithCharacter
。
答案 1 :(得分:2)
如何从一个公共抽象类派生所有可碰撞对象(让我们称之为Collidable)。该类可以包含可以通过collission和一个HandleCollision函数更改的所有属性。当两个对象发生碰撞时,您只需在每个对象上调用HandleCollision,另一个对象作为参数。每个对象操纵另一个对象来处理碰撞。这两个对象都不需要知道它刚刚弹回的其他对象类型,并且没有大的switch语句。
答案 2 :(得分:1)
使所有可colidable实体使用collideWith(Collidable)方法实现接口(假设为“Collidable”)。 然后,在您的碰撞检测算法上,如果您检测到A与B发生碰撞,您将调用:
A->collideWith((Collidable)B);
B->collideWith((Collidable)A);
假设A是MainCharacter而B是怪物,并且都实现了Collidable接口。
A->collideWith(B);
将调用以下内容:
MainCharacter::collideWith(Collidable& obj)
{
//switch(obj.GetType()){
// case MONSTER:
// ...
//instead of this switch you were doing, dispatch it to another function
obj->collideWith(this); //Note that "this", in this context is evaluated to the
//something of type MainCharacter.
}
这将调用Monster :: collideWith(MainCharacter)方法,你可以在那里实现所有怪物角色行为:
Monster::CollideWith(MainCharacter mc){
//take the life of character and make it bounce back
mc->takeDamage(this.attackPower);
mc->bounceBack(20/*e.g.*/);
}
更多信息:Single Dispatch
希望它有所帮助。
答案 3 :(得分:1)
你所说的“烦人的开关声明”我称之为“一场精彩的比赛”,所以你走在正确的轨道上。
每个交互/游戏规则都有一个功能正是我所建议的。它使查找,调试,更改和添加新功能变得容易:
void PlayerCollidesWithWall(player, wall) {
player.velocity = 0;
}
void PlayerCollidesWithHPPotion(player, hpPoition) {
player.hp = player.maxHp;
Destroy(hpPoition);
}
...
所以问题是如何检测这些案例中的每一个。假设你有某种碰撞检测导致X和Y碰撞(就像N ^ 2重叠测试一样简单(嘿,它适用于植物大战和僵尸,而且还有很多事情发生!)或者像扫描和修剪一样复杂+ gjk)
void DoCollision(x, y) {
if (x.IsPlayer() && y.IsWall()) { // need reverse too, y.IsPlayer, x.IsWall
PlayerCollidesWithWall(x, y); // unless you have somehow sorted them...
return;
}
if (x.IsPlayer() && y.IsPotion() { ... }
...
这种风格虽然详细,但
Spore的细胞阶段正好使用这种风格并且大约有100次检查,导致大约70种不同的结果(不包括参数反转)。这只是一个十分钟的游戏,这是整个舞台每6秒1次新的互动 - 现在这是游戏玩法的价值!
答案 4 :(得分:0)
如果我正确地解决了你的问题,我会像
一样Class EventManager {
// some members/methods
handleCollisionEvent(ObjectType1 o1, ObjectType2 o2);
// and do overloading for every type of unique behavior with different type of objects.
// can have default behavior as well for unhandled object types
}