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

8

13

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 and 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?

Rozella answered 1/2, 2010 at 19:57 Comment(9)
What's wrong with instance methods? You should be doing something more like source.PerformAction(target).Uncalledfor
Honestly I have no clue I've always wondered how game developers were ever able to make that work for creatures to maintain their AI along with the logic that maps where items are fallen on the ground (or blood splatters / corpses especially if they're persistent) along with the visual presentation of the game and all that entails.Stigmatism
@Chris: Modularity. The AI has little-to-nothing to do with the graphics, and it's usually in separate code entirely with different developers working on it.Uncalledfor
This is a bit too open-ended for me to give any useful suggestions, but I do want to point out that you seem to be trying for double dispatch.Strick
@Anon I understand the overall notion of how AI is implemented that each creature really is given it's own construct of what it's AI is and then it's free to interact with the world in the way the AI dictates but more of the aspect of having that happen on the scale it does in games with possibly 100s of on screen characters need their position in the world maintained along with the position of everything else that exists above the "static" base world amazes me that it can be achieved together in computing power especially when it's scaled out to WoW sized worlds.Stigmatism
@Chris: WoW doesn't have to deal much with global interactions. Instead, characters are in zones.Strick
Computers are pretty dang fast these days. Running a simple script and updating a handful of values for a few hundred entities is chips for any reasonably modern processor. Also note that the CPU does very little in terms of graphics handling these days - we have specialized hardware designed almost for the sole purpose of rendering polygons incredibly quickly.Uncalledfor
If only that last statement were true - probably most of the CPU time in most games is still spent on graphics handling. Luckily MMO servers don't need to worry about that part.Bobbette
You may find the discussion of the command pattern at #361502 to be of interest.Youngling
S
4

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.

Scheffler answered 1/2, 2010 at 20:3 Comment(1)
BTW, an interesting variation is the "Acyclic Visitor." You can read more about it here: objectmentor.com/resources/articles/acv.pdfScheffler
L
3

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.

Lighting answered 1/2, 2010 at 20:1 Comment(0)
T
3

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.

Thurlow answered 5/2, 2010 at 20:41 Comment(2)
So far I really like this approach. It feels like a simplified component based entity design. However, do all classes HAVE to derive from Entity? As long as the right interfaces are added can't I just have them derive from any class?Rozella
No, they don't. It may be helpful for them to for other reasons (there may be stuff all entities in the game have in common that you want to be able to assume). Otherwise, you could just pass in object and it would work fine.Thurlow
B
2

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. :)

Bobbette answered 2/2, 2010 at 15:9 Comment(0)
S
1

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...

Section answered 1/2, 2010 at 20:1 Comment(0)
P
0

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
Pleadings answered 1/2, 2010 at 20:1 Comment(0)
L
0

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.

Liquor answered 1/2, 2010 at 20:8 Comment(0)
V
0

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):

Table showing relationships between different values

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!

Vasilikivasilis answered 15/9, 2011 at 23:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.