22
\$\begingroup\$

I'm an inexperienced programmer creating a "roguelike-like" game in the vein of FTL, using Python (no PyGame as of yet as I'm still only concerned with text).

My game will contain a large number of weapons (around 50 for starters) that yield unique abilities. I am struggling to understand how to structure the object code in a way that is both powerful (in terms of allowing weapons to have radically different effects) and extensible (so that I can easily add more weapons later by eg. dropping them into a folder).

My first instinct was to have a BasicWeapon class, and have different weapons inherit from that class. However, this seems problematic to me: either I have to make the BasicWeapon class so barebones that it's basically useless (the only features all weapons have in common are name and type (pistol, axe, etc)), or I have to predict every unique effect I will ever come up with and code that into BasicWeapon.

The latter is clearly impossible, but the former can still be worked. However, that leaves me with the question: where do I put the code for individual weapons?

Do I create plasmarifle.py, rocketlauncher.py, swarmofbees.py, etc etc, and drop them all into a folder from where the game can import them?

Or is there a way to have a database-style file (maybe something as simple as an Excel spreadsheet) that somehow contains unique code for each weapon - without needing to resort to eval/exec?

In terms of the latter solution (database), I think the fundamental issue that I'm struggling with is that while I understand that it is desirable to maintain separation between code and data, I feel like the weapons blur the line between "code" and "data" a little bit; they represent the great variety of similar things that can be found in the game, in which sense they are like data, but most of them will require at least some unique code not shared with any other item, in which sense they are, naturally, code.

A partial solution I have found elsewhere on this site suggests giving the BasicWeapon class a bunch of empty methods - on_round_start(), on_attack(), on_move() etc - and then overriding those methods for each weapon. At the relevant phase of the combat cycle, the game will call the appropriate method for every character's weapon, and only the ones that have methods defined will actually do something. This helps, but it still does not tell me where I must put the code and/or data for each weapon.

Is there a different language or tool out there that I can use as a sort of half-data, half-code chimera? Am I completely butchering good programming practice?

My understanding of OOP is sketchy at best, so I would appreciate responses that are not too computer science-y.

EDIT: Vaughan Hilts has made it clear in his post below that what I'm essentially talking about is data-driven programming. The essence of my question is this: how can I implement a data-driven design in such a way that the data can contain scripts, enabling new weapons to do new things without changing the main program code?

\$\endgroup\$
4
  • 3
    \$\begingroup\$ Related: gamedev.stackexchange.com/questions/17276/… \$\endgroup\$
    – House
    Mar 11, 2013 at 22:11
  • \$\begingroup\$ @Byte56 Related; but I think this is what the OP is trying to avoid. I think they're trying to find a more data driven approach. Correct me if I'm wrong. \$\endgroup\$ Mar 11, 2013 at 22:12
  • \$\begingroup\$ I agree they're trying to find a more data oriented approach. Specifically, I like Josh's answer to that question: gamedev.stackexchange.com/a/17286/7191 \$\endgroup\$
    – House
    Mar 11, 2013 at 22:15
  • \$\begingroup\$ Ah, sorry about that. :) I have a bad habit of reading the "accepted answer". \$\endgroup\$ Mar 11, 2013 at 22:24

3 Answers 3

17
\$\begingroup\$

You want a data-driven approach almost certainly unless your game is going to be completely un-expected and/or procedural generated to the core.

Essentially, this involves storing information about your weapons in a markup language or file format of your choice. XML and JSON are both good, readable choices that can be used to make editing fairly simple without the need for complicated editors if you're just trying to get a quick start. (And Python can parse XML pretty easy, too!) You'd set attribute like 'power', 'defense', 'cost', and 'stats' that are all relevant. The way you structure your data will be up to you.

If a weapon needs to add a status effect, give it a Status effect node, and then specify the effects of a status effect through another data-driven object. This will make your code less reliant on the specific game and making editing and testing your game trivial. Not having to recompile all the time is a bonus, too.

Supplemental reading is available below:

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Kind of like a component based system, where the components are read in through scripts. Like this: gamedev.stackexchange.com/questions/33453/… \$\endgroup\$
    – House
    Mar 11, 2013 at 22:17
  • 2
    \$\begingroup\$ And while you're at it, make a script part of that data so that new weapons can do new things with no main code changes. \$\endgroup\$ Mar 12, 2013 at 3:57
  • \$\begingroup\$ @Vaughan Hilts: thank you, data-driven seems to be exactly what I intuitively understood I needed. I'm leaving the question open for a while longer as I still need answers, but will probably choose this as the best answer. \$\endgroup\$
    – henrebotha
    Mar 12, 2013 at 7:21
  • \$\begingroup\$ @Patrick Hughes: that's exactly what I want! How do I do that? Can you show me a simple example or tutorial? \$\endgroup\$
    – henrebotha
    Mar 12, 2013 at 7:22
  • 1
    \$\begingroup\$ First you need a script engine in your engine, many people choose LUA, that accesses gameplay systems like effects and statistics. Then since you're already recreating your objects from a data description you can embed the script that your engine calls whenever your new object is activated. In the olden days of MUDs this was called a "proc," (short for Process). The hard part is making your gameplay features in the engine flexible enough to be called from outside and with enough features. \$\endgroup\$ Mar 12, 2013 at 14:37
7
\$\begingroup\$

(I'm sorry to submit the answer instead of a comment, but I don't have rep yet.)

Vaughan's answer is great, but I'd like to add my two cents.

One of the main reasons you'd want to use XML or JSON and parse it in runtime is to change and experiment with new values without having to recompile the code. As Python is interpreted and, in my opinion, pretty readable, you could have the raw data in a file with a dictionary and everything organized:

weapons = {
           'megaLazer' : {
                          'name' : "Mega Lazer XPTO"
                          'damage' : 100
                       },
           'ultraCannon' : {
                          'name' : "Ultra Awesome Cannon",
                          'damage' : 200
                       }
          }

This way you just import the file/module and use it as a normal dictionary.

If you want to add scripts, you can make use of the dynamic nature of Python and 1st class functions. You could do something like this:

def special_shot():
    ...

weapons = { 'megalazer' : { ......
                            shoot_gun = special_shot
                          }
          }

Although I believe that would be against data driven design. To be 100% DDD you'd have infomation (data) specifying what would be the functions and code that specific weapon would use. This way you don't break DDD, as you don't mix data with functionality.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you. Just seeing a simple code example helped it to click. \$\endgroup\$
    – henrebotha
    Mar 13, 2013 at 16:06
  • 1
    \$\begingroup\$ +1 for the nice answer and for you to have enough rep to comment. ;) Welcome. \$\endgroup\$
    – ver
    Mar 21, 2013 at 10:37
4
\$\begingroup\$

Data-Driven Design

I submitted something like this question to code review recently.

After some suggestions and improvements, the result was a simple code that would allow some relative flexibility on weapon creation based on a dictionary (or JSON). The data is interpreted at runtime and simple verifications are done by the Weapon class itself, without the need to rely on a whole script interpreter.

Data-Driven Design, despite Python being an interpreted language (both source and data files can be edited without the need to recompile them), sounds like the right thing to do in cases such as the one you presented. This question goes into more details about the concept, its pros and cons. Theres also a nice presentation on Cornell University about it.

Compared with other languages, such as C++, that would probably use a scripting language (such as LUA) to handle data x engine interaction and scripting in general, and a certain data format (like XML) to store the data, Python can actually do it all on its own (considering the standard dict but also weakref, the latter specifically for resource loading and caching).

An independent developer, however, may not take the data-driven approach to its extreme as suggested on this article:

How much about data-driven design am I? I don't think a game engine should contain a single line of game-specific code. Not one. No hardcoded weapon types. No hardcoded HUD layout. No hardcoded unit AI. Nada. Zip. Zilch.

Maybe, with Python, one could benefit from the best of both object-oriented and data-driven approach, aiming for both productivity and extensibility.

Simple sample processing

In the specific case discussed on code review, a dictionary would store both the "static attributes" and the logic to be interpreted - should the weapon have any conditional behavior.

On the example below a sword should have some abilities and stats in the hands of characters of the class 'antipaladin', and no effects, with lower stats when used by other characters):

WEAPONS = {
    "bastard's sting": {
        # magic enhancement, weight, value, dmg, and other attributes would go here.
        "magic": 2,

        # Those lists would contain the name of effects the weapon provides by default.
        # They are empty because, in this example, the effects are only available in a
        # specific condition.    
        "on_turn_actions": [],
        "on_hit_actions": [],
        "on_equip": [
            {
                "type": "check",
                "condition": {
                    'object': 'owner',
                    'attribute': 'char_class',
                    'value': "antipaladin"
                },
                True: [
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_hit",
                            "actions": ["unholy"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "add_to",
                        "args": {
                            "category": "on_turn",
                            "actions": ["unholy aurea"]
                        }
                    },
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 5
                        }
                    }
                ],
                False: [
                    {
                        "type": "action",
                        "action": "set_attribute",
                        "args": {
                            "field": "magic",
                            "value": 2
                        }
                    }
                ]
            }
        ],
        "on_unequip": [
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_hit",
                    "actions": ["unholy"]
                },
            },
            {
                "type": "action",
                "action": "remove_from",
                "args": {
                    "category": "on_turn",
                    "actions": ["unholy aurea"]
                },
            },
            {
                "type": "action",
                "action": "set_attribute",
                "args": ["magic", 2]
            }
        ]
    }
}

For testing purposes, I created simple Player and Weapon classes: the first to hold/equip the weapon (thus calling its conditional on_equip setting) and the latter as a single class that would retrieve the data from the dictionary, based on the item name passed as an argument during the Weaponinitialization. They do not reflect the proper game classes design, but can still be useful to test the data:

class Player:
    """Represent the player character."""

    inventory = []

    def __init__(self, char_class):
        """For this example, we just store the class on the instance."""
        self.char_class = char_class

    def pick_up(self, item):
        """Pick an object, put in inventory, set its owner."""
        self.inventory.append(item)
        item.owner = self


class Weapon:
    """A type of item that can be equipped/used to attack."""

    equipped = False
    action_lists = {
        "on_hit": "on_hit_actions",
        "on_turn": "on_turn_actions",
    }

    def __init__(self, template):
        """Set the parameters based on a template."""
        self.__dict__.update(WEAPONS[template])

    def toggle_equip(self):
        """Set item status and call its equip/unequip functions."""
        if self.equipped:
            self.equipped = False
            actions = self.on_unequip
        else:
            self.equipped = True
            actions = self.on_equip

        for action in actions:
            if action['type'] == "check":
                self.check(action)
            elif action['type'] == "action":
                self.action(action)

    def check(self, dic):
        """Check a condition and call an action according to it."""
        obj = getattr(self, dic['condition']['object'])
        compared_att = getattr(obj, dic['condition']['attribute'])
        value = dic['condition']['value']
        result = compared_att == value

        self.action(*dic[result])

    def action(self, *dicts):
        """Perform action with args, both specified on dicts."""
        for dic in dicts:
            act = getattr(self, dic['action'])
            args = dic['args']
            if isinstance(args, list):
                act(*args)
            elif isinstance(args, dict):
                act(**args)

    def set_attribute(self, field, value):
        """Set the specified field with the given value."""
        setattr(self, field, value)

    def add_to(self, category, actions):
        """Add one or more actions to the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action not in action_list:
                action_list.append(action)

    def remove_from(self, category, actions):
        """Remove one or more actions from the category's list."""
        action_list = getattr(self, self.action_lists[category])

        for action in actions:
            if action in action_list:
                action_list.remove(action)

With some future improvement I hope that this will even allow me to have a dynamic crafting system someday, processing weapon components instead of whole weapons...

Test

  1. Character A picks weapon, equip it (we print its stats), then drop it;
  2. Character B picks the same weapon, equip it (and we print its stats again to show how they are different).

Like this:

def test():
    """A simple test.

    Item features should be printed differently for each player.
    """
    weapon = Weapon("bastard's sting")
    player1 = Player("bard")
    player1.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))
    weapon.toggle_equip()

    player2 = Player("antipaladin")
    player2.pick_up(weapon)
    weapon.toggle_equip()
    print("Enhancement: {}, Hit effects: {}, Other effects: {}".format(
        weapon.magic, weapon.on_hit_actions, weapon.on_turn_actions))

if __name__ == '__main__':
    test()

It should print:

For a bard

Enhancement: 2, Hit effects: [], Other effects: []

For a antipaladin

Enhancement: 5, Hit effects: ['unholy'], Other effects: ['unholy aurea']

\$\endgroup\$

You must log in to answer this question.

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