开发者

Help on implementing how creatures and items interact in a computer role playing game

开发者 https://www.devze.com 2022-12-19 07:53 出处:网络
I am programming a simple role playing game (to learn and for fun) and I\'m at the point where I\'m trying to come up with a way for game objects to interact with each other. There are two things I am

I am programming a simple role playing game (to learn and for fun) and I'm at the point where I'm trying to come up with a way for game objects to interact with each other. There are two things I am trying to avoid.

  1. Creating a gigantic game object that can be anything and do everything
  2. Complexity - so I am staying away from a component based design like you see here

So with those parameters in mind I need advice on a good way for game objects to perform actions on each other.

For example

  • Creatures (Characters, Monsters, NPCs) can perform actions on Creatures or Items (weapons, potions, traps, doors)
  • Items can perform actions on Creatures or Items as well. An example would be a trap going off when a character tries to open a chest

What I've come up with is a PerformAction method that can take Creatures or Items as parameters. Like this

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

Or should I do this instead?

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

Or is there a completely different way I should be thinking about this?


This is a "double dispatch" problem. In regular OO programming, you "dispatch" the operation of a virtual method call to the concrete type of the class implementing the object instance you call against. A client doesn't need to know the actual implementation type, it is simply making a method call against an abstract type description. That's "single dispatch".

Most OO languages don't implement anything but single-dispatch. Double-dispatch is when the operation that needs to be called depends on two different objects. The standard mechanism for implementing double dispatch in OO languages without direct double-dispatch support is the "Visitor" design pattern. See the link for how to use this pattern.


This sounds like a case for polymorphism. Instead of taking Item or Creature as an argument, make both of them derive (or implement) from ActionTarget or ActionSource. Let the implementation of Creature or Item determine which way to go from there.

You very rarely want to leave it so open by just taking Object. Even a little information is better than none.


You can try mixing the Command pattern with some clever use of interfaces to solve this:

// 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);
}

Then a derived Command downcasts to see if it can do what it needs to the target:

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);
        }
    }
}

Note that:

  1. A derived Entity only has to implement the capabilities that are appropriate for it.

  2. The base Entity class doesn't have code for any capability. It's nice and simple.

  3. Commands can gracefully do nothing if an entity is unaffected by it.


I think you're examining too small a part of the problem; how do you even determine the arguments to the PerformAction function in the first place? Something outside of the PerformAction function already knows (or somehow must find out) whether the action it wants to invoke requires a target or not, and how many targets, and which item or character it's operating upon. Crucially, some part of the code must decide what operation is taking place. You've omitted that from the post but I think that is the absolute most important aspect, because it's the action that determines the required arguments. And once you know those arguments, you know the form of the function or method to invoke.

Say a character has opened a chest, and a trap goes off. You presumably already have code which is an event handler for the chest being opened, and you can easily pass in the character that did it. You also presumably already ascertained that the object was a trapped chest. So you have the information you need already:

// pseudocode
function on_opened(Character opener)
{
  this.triggerTrap(opener)
}

If you have a single Item class, the base implementation of triggerTrap will be empty, and you'll need to insert some sort of checks, eg. is_chest and is_trapped. If you have a derived Chest class, you'll probably just need is_trapped. But really, it's only as difficult as you make it.

Same goes for opening the chest in the first place: your input code will know who is acting (eg. the current player, or the current AI character), can determine what the target is (by finding an item under the mouse, or on the command line), and can determine the required action based on the input. It then simply becomes a case of looking up the right objects and calling the right method with those arguments.

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

Keep it simple. :)


in the Zork model, each action one can do to an object is expressed as a method of that object, e.g.

door.Open()
monster.Attack()

something generic like PerformAction will end up being a big ball of mud...


What about having a method on your Actors (creatures, items) that Perform the action on a target(s). That way each item can act differently and you won't have one big massive method to deal with all the individual items/creatures.

example:

public abstract bool PerformAction(Object target);  //returns if object is a valid target and action was performed


I've had a similar situation to this, although mine wasn't Role playing, but devices that sometimes had similar characteristics to other devices, but also some characteristics that are unique. The key is to use Interfaces to define a class of actions, such as ICanAttack and then implement the particular method on the objects. If you need common code to handle this across multiple objects and there's no clear way to derive one from the other then you simply use a utility class with a static method to do the implementation:

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) { ... }
}

Then if you have code that needs to determine whether a character can or can't do something:

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

This way, you explicitly know what capabilities any particular type of character has, you get good code reuse, and the code is relatively self-documenting. The main drawback to this is that it is easy to end up with objects that implement a LOT of interfaces, depending on how complex your game is.


This might not be something that many would agree upon, but I'm not a team and it works for me (in most cases).

Instead of thinking of every Object as a collection of stuff, think of it as a collection of references to stuff. Basically, instead of one huge list of many

Object
    - Position
    - Legs
    - [..n]

You would have something like this (with values stripped, leaving only relationships):

Help on implementing how creatures and items interact in a computer role playing game

Whenever your player (or creature, or [..n]) wants to open a box, simply call

Player.Open(Something Target); //or
Creature.Open(Something Target); //or
[..n].Open(Something Target);

Where "Something" can be a set of rules, or just an integer which identifies the target (or even better, the target itself), if the target exists and indeed can be opened, open it.

All this can (quite) easily be implemented through a series of, say interfaces, like this:

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

interface IDamageable
{
      void Damage(
            int A
      );
}

With clever usage of these interfaces you might even ending up using stuff like delegates to make an abstraction between top-level

IDamageable

and the sub-level

IBurnable

Hope it helped :)

EDIT: This was embarassing, but it seems I hijacked @munificent's answer! I'm sorry @munificent! Anyway, look at his example if you want an actual example instead of an explanation of how the concept works.

EDIT 2: Oh crap. I just saw that you clearly stated you didn't want any of the stuff that was contained in the article you linked, which clearly is exactly the same as I have written about here! Disregard this answer if you like and sorry for it!

0

精彩评论

暂无评论...
验证码 换一张
取 消