如何在电脑角色扮演游戏中实现生物和物品互动的帮助

13

我正在编写一个简单的角色扮演游戏(用于学习和娱乐),目前我打算想出一种游戏对象之间互动的方法。我有两个需要避免的问题:

  1. 创建一个可以成为任何东西并且可以做任何事情的巨大游戏对象
  2. 复杂性 - 所以我避免使用像您在这里看到的基于组件的设计。

因此,在这些参数的基础上,我需要关于游戏对象之间执行操作的良好建议。

例如

  • 生物(角色、怪物、NPC)可以对其他生物或物品(武器、药水、陷阱、门等)执行操作
  • 物品也可以对生物或物品执行操作。例如,当角色试图打开一个箱子时,陷阱会触发

我想到的是一个PerformAction方法,它可以接受生物或物品作为参数。像这样

PerformAction(Creature sourceC, Item sourceI, Creature targetC, Item targetI)
// this will usually end up with 2 null params since
// only 1 source and 1 target will be valid

还是我应该这样做?

PerformAction(Object source, Object target)
// cast to correct types and continue

或者说,我应该有完全不同的思路吗?


3
实例方法有什么问题?你应该使用类似于 source.PerformAction(target) 的方式进行操作。 - Anon.
说实话,我一点头绪都没有。我一直想知道游戏开发人员是如何让生物保持他们的AI,同时还要处理地图上物品掉落的逻辑(或者血迹/尸体,特别是如果它们是持久的),以及游戏的视觉呈现和所有相关内容。 - Chris Marisic
@Chris:模块化。人工智能与图形几乎没有任何关系,通常是完全分开的代码,由不同的开发人员负责。 - Anon.
这对我来说有点太开放了,无法给出任何有用的建议,但我想指出你似乎正在尝试双重分派。 - Steven Sudit
现在的计算机速度非常快。对于任何一个相当现代的处理器来说,运行一个简单的脚本并更新几百个实体的少量值都是小菜一碟。此外,请注意,CPU 在图形处理方面的作用很小 - 我们有专门设计用于以惊人的速度渲染多边形的硬件。 - Anon.
显示剩余4条评论
8个回答

4
这是一个“双重派遣”问题。在常规面向对象编程中,您将虚拟方法调用的操作分派给实现您对其调用的对象实例的类的具体类型。客户端不需要知道实际实现类型,它只是针对抽象类型描述进行方法调用。这就是“单一派遣”。
大多数面向对象语言只实现了单一派遣,而双重派遣是需要调用两个不同对象的操作。在没有直接双重派遣支持的面向对象语言中实现双重派遣的标准机制是“访问者”设计模式。请参见链接以了解如何使用此模式。

顺便提一下,一个有趣的变体是“非循环访问者(Acyclic Visitor)”。您可以在这里阅读更多信息:http://www.objectmentor.com/resources/articles/acv.pdf - David Gladfelter

3
您可以尝试将命令模式与接口的巧妙使用相结合来解决这个问题:
// everything in the game (creature, item, hero, etc.) derives from this
public class Entity {}

// every action that can be performed derives from this
public abstract class Command
{
    public abstract void Perform(Entity source, Entity target);
}

// these are the capabilities an entity may have. these are how the Commands
// interact with entities:
public interface IDamageable
{
    void TakeDamage(int amount);
}

public interface IOpenable
{
    void Open();
}

public interface IMoveable
{
    void Move(int x, int y);
}

然后,派生的 Command 进行向下转换,以查看它是否可以对目标执行所需的操作:

public class FireBallCommand : Command
{
    public override void Perform(Entity source, Entity target)
    {
        // a fireball hurts the target and blows it back
        var damageTarget = target as IDamageable;
        if (damageTarget != null)
        {
            damageTarget.TakeDamage(234);
        }

        var moveTarget = target as IMoveable;
        if (moveTarget != null)
        {
            moveTarget.Move(1, 1);
        }
    }
}

请注意:
  1. 派生实体只需要实现适合它的功能。

  2. 基础实体类没有任何能力的代码。这很好,也很简单。

  3. 如果实体不受命令影响,则命令可以优雅地不执行任何操作。


到目前为止,我真的很喜欢这种方法。它感觉像是一个简化的基于组件的实体设计。然而,所有类都必须从“Entity”派生吗?只要添加了正确的接口,我不能让它们从任何类派生吗? - mikedev
不,他们不需要。这可能对他们有其他方面的帮助(游戏中可能存在所有实体都具有共同点,您希望能够假定这些共同点)。否则,您可以只传递对象,它也可以正常工作。 - munificent

3
这似乎是多态的一个案例。不要将Item或Creature作为参数,而是使它们都派生(或实现)自ActionTarget或ActionSource。让Creature或Item的具体实现决定从哪里开始。很少情况下你希望只使用Object这样开放的方式。即使只有一点信息也比没有好。

2
我认为您只考虑了问题的一小部分;首先,如何确定PerformAction函数的参数?PerformAction函数之外的某些东西已经知道(或必须找出)它想要调用的操作是否需要目标、需要多少个目标以及它正在操作哪个项目或角色。至关重要的是,代码的某个部分必须决定正在进行的操作。您在帖子中省略了这一点,但我认为这是绝对最重要的方面,因为正是操作确定了所需的参数。一旦您知道这些参数,就会知道要调用的函数或方法的形式。
比如一个角色打开了一个箱子,然后触发了一个陷阱。您可能已经有了处理箱子被打开的事件的代码,并且可以轻松地传递打开箱子的角色。您也可能已经确定该对象是一个有陷阱的箱子。所以您已经拥有所需的信息。
// pseudocode
function on_opened(Character opener)
{
  this.triggerTrap(opener)
}

如果你只有一个Item类,triggerTrap的基本实现将是空的,你需要插入一些检查,例如is_chestis_trapped。如果你有一个派生的Chest类,你可能只需要is_trapped。但实际上,它只有你自己想得难。同样适用于首先打开箱子:输入代码将知道谁在行动(例如当前玩家或当前AI角色),可以通过找到鼠标下面的物品或在命令行上找到物品来确定目标,并且可以根据输入确定所需的操作。然后只需查找正确的对象并使用这些参数调用正确的方法即可。
item = get_object_under_cursor()
if item is not None:
    if currently_held_item is not None:
        player_use_item_on_other_item(currently_held_item, item)
    else
        player.use_item(item)
    return

character = get_character_under_cursor()
if character is not None:
    if character.is_friendly_to(player):
        player.talk_to(character)
    else
        player.attack(character)
    return

保持简单。 :)

1
在Zork模型中,每个可以对对象执行的操作都表示为该对象的方法,例如:
door.Open()
monster.Attack()

像PerformAction这样的通用操作最终会变成一个庞大的混乱代码块...


0
在你的演员(生物、物品)上拥有一个执行动作的方法,对于目标进行操作怎么样?这样每个物品可以有不同的行为,而你也不必处理所有单独的物品/生物的大量方法。
例如:
public abstract bool PerformAction(Object target);  //returns if object is a valid target and action was performed

0

我曾经遇到过类似的情况,虽然我的不是角色扮演,而是有些设备具有与其他设备类似的特征,但也有一些独特的特征。关键是使用接口来定义一类操作,例如ICanAttack,然后在对象上实现特定的方法。如果您需要处理多个对象的公共代码,并且没有明显的从一个对象派生另一个对象的方法,那么您只需使用带有静态方法的实用程序类来执行实现:

public interface ICanAttack { void Attack(Character attackee); }
public class Character { ... }
public class Warrior : Character, ICanAttack 
{
    public void Attack(Character attackee) { CharacterUtils.Attack(this, attackee); }
}
public static class CharacterUtils 
{
    public static void Attack(Character attacker, Character attackee) { ... }
}

如果您有需要确定字符能否执行某些操作的代码:

public void Process(Character myCharacter)
{
    ...
    ICanAttack attacker = null;
    if ((attacker = (myCharacter as ICanAttack)) != null) attacker.Attack(anotherCharacter);
}

这种方式可以明确知道任何特定类型的字符具有哪些功能,可以实现良好的代码重用,并且代码相对自我记录。但是,主要缺点是,根据您的游戏有多复杂,很容易出现实现了大量接口的对象。


0

这可能不是许多人都会同意的事情,但对我来说很有效(在大多数情况下)。

与其将每个对象视为一组“东西”,不如将其视为一组指向“东西”的“引用”。基本上,而不是一个巨大的列表。

Object
    - Position
    - Legs
    - [..n]

你会得到类似这样的东西(值被剥离,只留下关系):

Table showing relationships between different values

无论您的玩家(或生物,或[..n])何时想要打开一个箱子,只需调用
Player.Open(Something Target); //or
Creature.Open(Something Target); //or
[..n].Open(Something Target);

其中,“Something”可以是一组规则,也可以只是标识目标的整数(甚至更好的是目标本身),如果存在目标并且确实可以打开,则打开它。

所有这些都可以通过一系列接口(例如)相对容易地实现,如下所示:

interface IDraggable
{
      void DragTo(
            int X,
            int Y
      );
}

interface IDamageable
{
      void Damage(
            int A
      );
}

通过巧妙地使用这些接口,您甚至可能最终使用委托之类的东西来在顶层之间建立抽象。
IDamageable

和子级

IBurnable

希望它有所帮助 :)
编辑:这很尴尬,但似乎我劫持了@munificent的答案!对不起@munificent!无论如何,如果你想要一个实际的例子而不是关于这个概念如何工作的解释,请看他的例子。
编辑2:哦,该死。我刚刚看到你明确表示你不想要任何与你链接的文章中包含的东西,这显然与我在这里写的完全相同!如果你愿意,请忽略这个答案,并为此道歉!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接