3
\$\begingroup\$

I'm working on a system to track large numbers of characters across long periods of time. Characters grow old and die, give birth to new characters, etc. It's a grand-strategy game, with warring nations and long scenarios.

Of course, this means I need to track the passage of years. My first idea was to create a class Calendar, with methods like increment_year() that would go through the list of every person, faction, etc in the game and call their methods (update_age() and so forth).

However, this would mean Calendar would be a singleton, which is sometimes considered an antipattern.

Alternatively, I could make a global variable for the current time, and update that as part of the main() game loop--using globals sounds like a dirty thing to do, though.

Is there a good reason to use a Calendar singleton (or even a reason to have multiple separate Calendars?)

\$\endgroup\$
  • 4
    \$\begingroup\$ Your Calendar example doesn't require the use of the Singleton pattern per se. The Game Programming Patterns entry argues that Singleton is often an anti-pattern because it combines single-instantiation with global access (which has the same issues as global variables), when often we only want one of the two. For your calendar, you can get single-instantiation without global access, and simply pass a reference to the calendar to anything that needs to modify time (often called dependency injection) \$\endgroup\$ – DMGregory Jan 17 '16 at 3:51
  • 1
    \$\begingroup\$ blog.stackoverflow.com/2011/08/gorilla-vs-shark \$\endgroup\$ – Vaillancourt Jan 17 '16 at 14:24
  • 2
    \$\begingroup\$ If you have to act upon a lot of data, maybe the object-oriented approach is not what you need, you could take a look into data-oriented design. \$\endgroup\$ – Vaillancourt Jan 17 '16 at 14:46
2
\$\begingroup\$

Thinking of something like ROTK series, every scenario has a starting date and would benefit from holding the game time as one of its attributes, considering that many of its methods would need to access it.

Also, if you think of saving/loading a game, the time should belong to the scenario on the data file - a scenario would then represent the whole game instance/session.

If we make the current scenario available to each creature, their age could become a property:

@property
def age(self):
    """Return timedelta // 365, age in years."""
    return (self.scenario.date - self.birth).days // 365

Thinking about a turn-based strategy game, for example, at each turn/step the scenario could check for births and deaths of the creatures on it. I've made some test code to illustrate. Even if it doesn't help you, at least it points out what I (mis)understood.

"""An illustration of a possible approach to handle time, births and deaths."""

import datetime


class Scenario:  # or level, or map, or scene, or session
    """A class to hold maps, creatures - and time."""

    creatures = []

    def __init__(self, year, month, day):
        """Start a new scenario.

        As every scenario is time related, we hold the time as one of its
        attributes.
        """
        self.date = datetime.date(year, month, day)

    def step(self, **kwargs):
        """The ammount of time we're goin to pass."""
        delta = datetime.timedelta(**kwargs)
        self.date = self.date + delta
        self.check_births()
        self.check_deaths()

    def add_creature(self, creature):
        """Add the new creature to the list of creatures."""
        self.creatures.append(creature)

    def remove_creature(self, creature):
        """Remove a dead creature from the list of creatures."""
        self.creatures.remove(creature)

    def check_births(self):
        """Check the fertility/married attributes - that we don't have.

        If they match the criteria, a new live is created!
        """
        pass

    def check_deaths(self):
        """Check if creatures have reached the maximum age.

        If so, they're dead!
        """
        for creature in self.creatures:
            if creature.age > 40:
                creature.die(cause="time")


class Creature:
    """A living creature that resides in the current scenario."""

    def __init__(self, name, year, month, day, scenario):
        """We pass in a name, a birth date and the current scenario."""
        self.name = name
        self.birth = datetime.date(year, month, day)
        self.scenario = scenario
        self.scenario.add_creature(self)

    @property
    def age(self):
        """Return timedelta // 365, age in years."""
        return (self.scenario.date - self.birth).days // 365

    def die(self, cause):
        """Time to die."""
        death_msgs = {
            "time": "At the age of {}, {} is no more."
        }
        print(death_msgs[cause].format(
            self.age, self.name
        ))
        self.scenario.remove_creature(self)

s = Scenario(year=1200, month=12, day=1)
c = Creature(name="John Doe", year=1160, month=10, day=5, scenario=s)

# the current age, 40
print(c.age)

# some time passes
# at 41 every single person dies in this dumb test
s.step(weeks=80)

This would print:

40

And then, after one step, when the character dies:

At the age of 41, John Doe is no more.

If you don't like the idea of passing the scenario to every creature as an attribute, another possible way would be to turn every method of Scenario into @classmethods (which sounds reasonable to me, considering that only a single instance of scenario would be used at a time). Maybe even a dynamic transition would sounds more doable in that way (say, after x years, we enter in another scenario time-range: instead of handling a new instance of Scenario we could fetch specific stuff from that other scenario and use it on our class Scenario.

Should that be the case, on the creature's methods we could call Scenario.date, Scenario.add_creature, etc. In that way it would have the same functionalities of a global variable, without being one. Is it considered a bad practice? Probably, as it would work pretty much like a global...

\$\endgroup\$
1
\$\begingroup\$

If you don't need to process something every single turn/year for every character (though you might do for other reasons) you could just store the birthdate of each character, then when you need to know or display the age, subtract this from the current date. This lets you know the age of every character without having to constantly process and update all of them. You may want a death date too from the sounds of it.

\$\endgroup\$
  • \$\begingroup\$ I think the "give birth to new characters" portion of the question might need some amount of regular processing across all characters, not just working out ages for those that are displayed, so that the game can decide when it's appropriate to add a new character into the population. \$\endgroup\$ – DMGregory Jan 17 '16 at 13:56
  • \$\begingroup\$ My idea was to have certain characters tagged as "keystones,: which would mean the game always updates them. Less important characters are sorted into other groups, which are updated less frequently. As soon as the player interacts with a character, they are 'promoted' to keystone. \$\endgroup\$ – Somatic Jan 17 '16 at 17:40
0
\$\begingroup\$

The Singleton pattern is sometimes(!) by some(!) people considered an anti-pattern because it takes measures to make it impossible for more than one object of the class to exist and also provides access to that object from everywhere turning it into a global variable in disguise. This does not mean that it is bad when you have an object where you usually only having one instance of even though it would be theoretically possible to have more than one.

I would not necessary call the class Calendar, but otherwise there is absolutely nothing anti-patterny about a class which controls the flow of time, is instanced at game start and is explicitly passed to those objects/methods which require a reference to it.

In general, you should be careful with blindly following "patterns" and dismissing "anti-patterns" when you don't fully understand why they are considered good or bad. Otherwise you will just be following a cargo cult. A good resource for learning about the rationales behind patterns and anti-patterns is programmers stackexchange.

\$\endgroup\$
0
\$\begingroup\$

I wouldn't use a calendar, I would just make time a property of your game world, then having an "advance time" method that does both, update the state of your large number of characters and advance the time property.

The world could contain the time and a list of characters (you probably need to insert and remove elements from this list quite frequently, so pay attention to the time complexity of insertions and deletions).

One thing to clarify is if the update of a character only depends on the state of the character (and the world in general) itself but not on the state of other characters? If not, you would have to store the old state of the characters before, update then the character states and only afterwards forget about the previous characters' states.

Also the order of aging, dieing, giving birth might influence the outcome (in one time step, do you first give birth and then die or first die and then give birth only if you didn't die before), but not very much, you just have to decide for an order.

In Python like syntax this would look like:

class World():

    def __init__(self):

        # time in your time format
        self.time = X

        # characters as list
        self.characters = [...]

    def advance_time(self):

        # new characters
        updated_characters = []

        # loop over all characters
        for character in self.characters:

            # update character (may or may not depend on other characters and time)
            update_character_function(character, self.characters, self.time)

            # if he hasn't died, keep him
            if character.is_alive():
                updated_characters.extend(character)

            # maybe he gives birth (may or may not depend on other characters and time)
            if gives_birth(character, self.characters, self.time):
                updated_characters.extend(newborn_character_function(character))

        # finally replace characters by updated characters
        self.characters = self.updated_characters

        # and update the time
        self.time = update_time_function(self.time)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.