Monday, January 15, 2018

architecture - Ways to track time and character ages across many years of game time?


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



Answer



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


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...