0
\$\begingroup\$

I have three classes Location, Item, Character.

Every Location needs to know what items and characters are currently present within itself, every Character and Item need to know what Location they're in, every Character needs to know what items they're holding, and every Item needs to know what Character its currently being held by.

class Location {
    items: Item[]
    characters: Character[]

    constructor(items: Item[], characters: Character[]) {
        this.items = items
        this.characters = characters
    }
}
class Item {
    location: Location
    heldBy: Character

    constructor(location: Location, heldBy: Character) {
        this.location = location
        this.heldBy = heldBy
    }
}
class Character {
    location: Location
    inventory: Item[]

    constructor(location: Location, inventory: Item[]) {
        this.location = location
        this.inventory = inventory
    }
}

These circular references are convenient for quick lookup and feel natural to reason about, but make for a more tricky construction setup - in order to make a single instance of any of the three classes, I must have already made instances of the other two, chicken before the egg.

With this approach, I will have to have two rounds of construction:

  1. Construct all three instances with every field set as empty or null initially
  2. Loop back over the instances to stitch them together
class Location {
    items: Item[]
    characters: Character[]
    constructor() {
        this.items = []
        this.characters = []
    }
}
class Item {
    location: Location | null
    heldBy: Character | null
    constructor() {
        this.location = null
        this.heldBy = null
    }
}
class Character {
    location: Location | null
    inventory: Item[]
    constructor() {
        this.location = null
        this.inventory = []
    }
}
let location = new Location()
let item = new Item()
let character = new Character()

location.items.push(item)
location.characters.push(character)
item.location = location
item.heldBy = character
character.location = location
character.inventory.push(item)

Is this an acceptable approach? Are there better alternatives?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ I'd be tempted to answer with a frame challenge here. Is it a law of the universe that items must know their owners, or is that an implementation choice we could change? Do you ever iterate items directly without reaching them via an owner, or other situations where the owner is known via context and can be injected in as needed? \$\endgroup\$
    – DMGregory
    Apr 7, 2023 at 3:01
  • \$\begingroup\$ @DMGregory I could implement some of these classes without them having to know about their owners, but it seems like that would make certain gameplay mechanics more tricky to implement. For example, if a character were to attack another character, we would need to know whether they are both in the same location. Or consider an Item with the effect of being on fire, which should render any character currently holding it to take damage. If these classes don't know about their parents, it seems like I would have to do some round-about acrobatic techniques to indirectly figure out who to affect. \$\endgroup\$ Apr 7, 2023 at 3:11
  • 2
    \$\begingroup\$ Does the Location need to care whether the entity inside it is a character or an item? If not, why not genericise the entities into a common base class (or common script) and have specific methods/scripts for each type. That way, everything can import the common entity script/class and that's all they need to know to interact with any entity generically. Those who are aware of more abstract concepts like characters and items can use them, as they reference the more specific scripts/child classes \$\endgroup\$
    – Basic
    Apr 7, 2023 at 3:35

2 Answers 2

1
\$\begingroup\$

It really is not a good idea to have Inventory with an array directly inside the Character. You should have a class Inventory to hold every method/data that works with the Inventory.

The Inventory should not know that is being held by a Character. You want to keep the possibility to be able to reuse the script for other aspect of your game such as Chest or Shop without requiring to create a whole Character for it.

The exact same thing is true for the Location. The Location should not know that it holds a Character but instead work with an appropriate interface. Otherwise, a Location will never be able to manipulate other objects and concepts.

If were to say more formally, I would say that you are violating the Dependency Inversion Principle from the SOLID principles. While it is not require to follow the principles, they are good guideline to find what is wrong with your architecture.

\$\endgroup\$
2
  • \$\begingroup\$ I use an Inventory class to store items in my actual application, I simplified it for the example. Next, if Items don't know which character they're being held by, how would I implement things like item Effects/Enchantments that apply buffs to the character every frame? Next, you say that Location should not know about Characters but instead work with an interface- could you elaborate more on this? Finally; My dependencies are being injected, even if they are circular and require this extra construction step of linking everything together, so how is it violating the DIP? Thanks for reply! \$\endgroup\$ Apr 7, 2023 at 9:00
  • \$\begingroup\$ Usually you apply effects of items not by iterating over all items in the universe and checking what to affect, but by iterating over all characters that can use items and iterating over the items they're using. So the owning character is known from the iteration context and can be passed down, rather than needing to be looked up from the item itself. Usually I implement this as an ApplyEffectTo(target) method, where the recipient is known and provided by the caller (say, the system that iterates over equipped gear on each character), not stored by the item. \$\endgroup\$
    – DMGregory
    Apr 7, 2023 at 12:13
0
\$\begingroup\$

Generally, you implement the buffs/enchantments taking effect when you equip the item. This can give rise to bugs such as:

Aztec monks with 1/55 HP no longer die when picking up or dropping a relic.

Source

This was caused by an implementation where a monk holding a relic was a different unit than a monk not holding a relic (this game allows for new technology discoveries upgrading existing units - like cavalry becoming mobile infantry when automobiles get invented). A rounding error caused the unit to appear to have less than 1 hit point and therefore it died.

One only has the Sword of Awesomeness glow if it is equipped and also if there is an orc nearby. Or that torches only give off light when equipped, not when stored in a backpack.

Your location class should only contain the x, y and z coordinates in your grid/world. I think it would be smarter to have Coordinates separate from Location. Think of it as "coordinates are a place" and "a location is a place with a name".

My suggested changes are:

class Coordinates {
    x: int; 
    y: int;
    z: int;
}
class Location {
    name: string; /* Example: this is the restaurant named W at the corner of High Street and Broadway */     
    coordinates: Coordinates;
    constructor(coordinates: Coordinates, name: string) {
        this.coordinates = coordinates
        this.name = name
    }
}
class Item {
    /* stuff like weight, is it magic, what icon to show, */
}
class Character {
    coordinates: Coordinates;
    inventory: Item[]; /* collection of all items you have */

    /* Include other relevant body positions in your game */
    rightHand: Item; /* equipped in right hand, also included in inventory */
    leftHand: Item;
    shirt: Item;
    helmet: Item; 

    constructor(coordinates: Coordinates, inventory: Item[]) {
        this.coordinates = coordinates;
        this.inventory = inventory;
        /* nothing is equipped, is it all in your backpack? */ 
    }
}

Answering questions like "who is at location x, y, z?" would require querying the coordinates of all characters & NPCs in the vicinity. You might want to consider "stuff laying on the ground" as some sort of NPC. Some games display anything on the ground as a little box or bag. Other games display things on the ground just as they display it when held.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .