Usage¶
What is it? (And an Example)¶
Instead of requiring inheritance, one can specify which attributes to operate on and any objec that has those attributes is operated on. That is, if a Player has an x and y attribute and an (unrelated) Enemy class has an x and y attribute you can have them both influenced by a World object. This avoids complicated inheritance hierarchies.
Here is a small example to illustrate the above:
from dataclasses import dataclass
from punyecs import World, requirements
w = World()
@dataclass
class Player:
x: float
y: float
@dataclass
class Enemy:
x: float
y: float
@requirements(w, {"x", "y"})
def move(e, dt):
e.x += 0.1
e.y += 0.1
player = Player(0.0, 0.0)
enemy = Enemy(1.0, 1.0)
w.add(player)
w.add(enemy)
w.update(1)
print(player.x)
# Prints 0.1
print(player.y)
# Prints 0.1
print(enemy.x)
# Prints 1.1
print(enemy.y)
# Prints 1.1
Be sure to read the comments! Observe the move function operates on both Player and Enemy even though they are unrelated.
Note
We pass 1 to w.update(...) because in the video game context we virtually always want to pass some change in time per frame of the object. To keep the example simple, we disregard this value, however, you should consider passing dt in the game context. (For example, in Pygame, a game loop might look something like:
Fine Grained Control: Querying¶
Returning to the example above, we may want various enemies to move like above but instead want to allow controller input for the player object. We can avoid influencing the player object by putting it in the excluded objects list. The function move becomes:
@requirements(w, {"x", "y"}, exclude_objs=[player])
def move(e, dt):
e.x += 0.1
e.y += 0.1
Then after every w.update(1) the player object will still remain at x=0.0, y=0.0.
An Alternative: Exclude Based on a Value¶
It could be that you have multiple characters that are controllable that are not the player. You could give them an attribute controller: bool = True. Then exclude an object if it has the controller attribute and the controller attribute is True with exclude_attr_vals. That is:
@requirements(w, {"x", "y"}, exclude_attr_vals={"controller": True})
def move(e, dt):
e.x += 0.1
e.y += 0.1
An Extension: Exclude Based on Attribute Predicates¶
Excluding based on the singular value of an attribute still might not be enough control. Suppose we have high level entities and low level entities and that most entities (except high level entities) are affected by gravity. More precisely, suppose high level enemies are those that have an attribute level > 50. Gravity typically operates on the y attribute so we may write something like:
@requirements(w, {"y"}, exclude_attr_funcs={"level": lambda lvl: lvl > 50})
def gravity(e):
e.y -= GRAVITY
The exclude_attr_funcs takes in a dictionary of attribute names and a function. This function takes one parameter, namely the attribute corresponding to the key. The function then determines if the attribute satisfies the predicate and, if so, excludes that entity from the group.
Excluding Based on Attributes¶
We may not care what value the attribute is and simply want to exclude the object if it has that attribute. For instance, we may have many different kinds of creatures. Most can follow the usual movement update function, but some creatures have a wiggle attribute. wiggle could be a Boolean, or even something more sophisticated like a function that describes how the creature wiggles.
To illustrate this consider:
from dataclasses import dataclass
from punyecs import World, requirements
w = World()
@dataclass
class Player:
x: float
y: float
@dataclass
class WalkingEnemy:
x: float
y: float
@dataclass
class Wiggler:
x: float
y: float
wiggle: lambda x: x + 2
@requirements(w, {"x", "y"}, exclude={"wiggle"})
def move(e, dt):
e.x += 0.1
e.y += 0.1
@requirements(w, {"wiggle", "x", "y"})
def wiggle(e, dt):
e.x = wiggle(e.x)
e.y = wiggle(e.y)
player = Player(0.0, 0.0)
enemy = Enemy(1.0, 1.0)
wiggler = Wiggle(3.0, 3.0)
w.add(player)
w.add(enemy)
w.add(wiggler)
w.update(1)
print(player.x)
# Prints 0.1
print(player.y)
# Prints 0.1
print(enemy.x)
# Prints 1.1
print(enemy.y)
# Prints 1.1
print(wiggler.x)
# Prints 5.0
print(wiggler.y)
# Prints 5.0