A callback-based component framework built the Blender Game Engine's Python api.
The Blender Game Engine has a very powerful python api, allowing the programmer to do nearly anything they might want to with the game engine, and is surprisingly flexible, allowing for a wide range of coding styles. This power and flexibility, however, comes at the cost of complexity: The engine provides little in the way of flow control, and writing simple, reuseable code can be complicated. In addition, keeping track of states in the game engine can become complicated in real-world use cases.
There not enough room for a full advocacy of component systems here, but [this article] (http://www.raywenderlich.com/24878/introduction-to-component-based-architecture-in-games) provides a good explanation, although it is somewhat dismissive of object-oriented programming, which can complement a component system nicely. BGELive uses an approach similar to the second one in the article.
Simply put, component systems allow you to easily reuse code between a variety of game objects without worrying about complex object inheritance trees.
While an entity componet system has it's strengths, as described by the aforementioned article, it is designed to be run from the engine level of a game, and while this could be mimiced by the Blender Game Engine, it ignores the existing design, in which each object has it's own script.
BGELive uses callbacks, run by each object during the logic phase. These callbacks can be swapped in and out at will, allowing for a virtually endless number of states in a game object. For example, a player object can have an enemy collision callback, which is temporarily removed after the player is hit, making them invincible. Meanwhile, a control callback will continue to run without being aware that the player has been hit. Callbacks can be any callable object: including a standard function, an object with a __call__
function, or a [closure] (http://www.shutupandship.com/2012/01/python-closures-explained.html), allowing each callback to store variables; In the previous example, the enemy hit callback could have stored the player's health.
Callbacks also allow for function nesting. This can be useful for fun things like having a callback that counts down before running another function. This function chaining it the backbone of BGELive.
For this example, we will write a simple object that takes damage on colliding with an object, and then turns invincible for one second before returning to normal.
In the game engine, this might typically be accomplished like this, assuming that there is a timer named timer
:
class player(KX_GameObject):
def __init__(self):
self.health = 100:
def run(self):
if self['timer'] > 1.0 and len( self.sensors['Collision'].hitObjectList > 0 ):
self.health -= 1
self['timer'] = 0
Using BGELive, one might write it like this:
from live.gameobject import Live_GameObject
class player(Live_GameObject):
def __init__(self):
self.logic_components.set( collide() )
def collide():
health = 100
def collide_callback(self, id):
if len( self.sensors['Collision'].hitObjectList > 0 ):
from live.components import timed
self.logic_components.set( suspend(1), id )
While this may seem like more code at first, it does several things that are useful in longer programs:
-
It's reuseable: Not only is the collision code a reusable function, but it uses the suspend component from the availible library to pause the function without concerning the coder with the specifics.
-
Variables are contained by the relevant compenent: While the example may be a bit contrived, this can help prevent the object from getting cluttered with attributes that are only rarely used, and only concern a single function. The suspend component works in this way
That's fair, and you don't have to use BGELive if you don't want to. However, BGELive expands on the Blender Game Engine in other ways too, adding new features to KX_GameObject, such as new options for the applyMovement and applyRotation methods, and a new applyScale method, and more will be added in the future. Best of all, since BGELive is built on top of the current api, you can take advantage of these without changing your coding style.
Installation is easy:
-
Download the zip file from the sidebar on the left
-
Drop the
live
folder into you project's script folder -
That's it
You can start by using the following template to convert a KX_GameObject into a Live_GameObject
from bge import logic
from live import Live_GameObject
class YOUR_OBJECT_NAME (Live_GameObject):
def __init__(self, obj):
super().__init__(obj)
def run():
obj = logic.getCurrentController().owner
if type(obj) == KX_GameObject:
obj = YOUR_OBJECT_NAME( obj )
obj.run()
# Add a compenent to your object:
self.logic_components.set( MY_COMPONENT )
#You can save an id reference to an object by assigning the return value from the function to a variable:
id = self.logic_components.set( MY_COMPONENT )
# Or you can set one manually:
self.logic_components.set( MY_COMPONENT, id='a component' )
# A component callback, at its simplest, if a function that takes two arguments—
# the object to which the compenent is attached, and an id that references the component (we will get to uses for this later):
def MY_COMPONENT(obj, id):
pass
# If you want to give the component additional arguments, you can use a callable object, instead:
class MY_CLASS_COMPONENT():
def __init__(self, MY_ARGUMENT):
self.MY_VARIABLE = MY_ARGUMENT
def __call__(self, obj, id):
# do something with self.MY_VARIABLE
# You can—of course—change this variable:
...
def __call__(self, obj, id)
self.MY_VARIABLE += 1
# Make sure you pass in an instance of the object to logic_callbacks in order to make sure it's __call__ed correctly:
self.logic_components.set( MY_CLASS_COMPONENT( MY_ARGUMENT ) )
# You can also use other methods of saving variables to your component. I'm fond of closures:
def MY_CLOSURE( MY_ARGUMENT1, MY_ARGUMENT2 ):
def MY_COMPONENT(obj, id):
# do something with MY_ARGUMENT1
nonlocal MY_ARGUMENT2
MY_ARGUMENT2 += 1
return MY_COMPONENT
# Similar to the above, you'll need to pass in the inner function by calling the other one:
self.logic_components.set( MY_CLOSURE( MY_ARGUMENT1, MY_ARGUMENT2 ) )
# Now we get to the primary use for callback ids, removing components.
# Doing so is simple:
self.logic_components.remove(id)
# This can be done from outside of a component, or within:
def self_removing_component(obj, id);
obj.logic_components.remove(id)
# Id's can also be used to retrieve a callback from an object:
component = self.logic_components.get(id)
# This could be useful if you want to interrupt a component briefly:
def interrupting_closure(fun):
def interrupting_component(obj, id):
obj.logic_components.set(fun, id=id)
...
self.logic_components.add( interupting_closure( self.logic_components.get(ID) ), id=ID)
# Compenents can be paused at scene level by adding a list of states to pause in via the 'pause_when' arugment when setting an argument:
self.logic_components.set( MY_COMPONENT, pause_when = ['scene_pause', 'cutscene'] )
# The pause state can then be set in the scene dictionary:
logic.getCurrentScene['pause_state'] = 'cutscene'
# By using multiple states, you could have diffent components pause in different circumstances.
# During a cutscene, for example, you might want to have your player no longer accept movement input, but still
# follow cutscene scripts. Meanwhile, both components should pause when the player hits the pause button:
self.logic_components.set( controls, pause_when = ['scene_pause', 'cutscene'] )
self.logic_components.set( cutscene_script, pause_when = ['scene_pause'] )
In addition to logic components, Live_GameObject expands upon the existing feature set of KX_GameObject.
# Both of these method been given the keyword argument per_second, allowing them to transform the objects in
# blender_units and radians per second, respecively, instead of per frame:
self.applyMovement([1,1,0], per_frame=True)
self.applyRotatation([0,0,pi], per_frame=True)
# You can now specify whether to use degrees or radians for applyRotation():
self.applyRotation([0,0,180], units="degrees")
self.applyRotatation([0,0,pi], units="radians")
# A new applyScale() method has been added. Arguments are the same as for applyMovement():
self.applyScale([1,1,1], True, per_frame=True)
A handful of premade components are currently availible in the live.components module:
# The timed component allows you to run a component for a fixed length of time (in seconds) before it is removed from the object:
self.logic_components.set( timed( MY_COMPONENT, 5 ) )
# Instead of removing the component from the object, you could replace it with another one using the next_fun argument:
self.logic_components.set( timed( MY_COMPONENT, 5, next_fun=OTHER_COMPONENT ) )
# If you want to have a component pause for a period of time, before reactivation, you can use the suspend() component
self.logic_components.set( suspend( 5, self.logic_components.get(ID), id=ID ) )
The move_to() component can be used to move and object to a given point in world space via one of three criteria: speed, time, and acceleration.
# To move the object at a given number of Blender units per second, use the 'speed' keyword:
self.logic_components.set( move_to( [1,1,0], speed = 0.1) )
# If you want an object to reach the point in a given number of seconds, use the 'time' keyword:
self.logic_components.set( move_to( [1,1,0], time = 10) )
# And if you want the object to accelerate to the point ( in Blender units/second/second), use 'accel':
self.logic_components.set( move_to( [1,1,0], accel = 0.01) )
# You can optional set a start speed for the object with 'current_speed':
self.logic_components.set( move_to( [1,1,0], accel = 0.01, current_speed = 0.1) )
# Or a maximum speed with 'max_speed':
self.logic_components.set( move_to( [1,1,0], accel = 0.01, max_speed = 0.1) )
BGELive also provides a series of helpers for common tasks in the live.helpers module:
# The Timer class provides simple countdown timers that can be created and deleted on the fly.
# Time is given in seconds:
timer = Timer( SECONDS )
# Getting the timer will give you the time remaining on the timer, so the timer:
ten_seconds = Timer( 10 )
# would return 5.5 in 4½ seconds, and 0.0 in 11 seconds
When you duplicate an object in the Blender editor, it is given a new name automatically. For example, duplicating the object 'Cube' in a new file will result in the object 'Cube.001'. This can become problematic when looking for an object by name in the Game Engine. BGELive provides several helpers to make this easier.
# If you want to get the clean name of an object, you can use the clean_name() function,
# which will return the name minus any auto-generated cruft
original_name = clean_name(obj)
# For finding an object in the scene, you can use the find_object() function:
object = find_object( 'ORIGINAL_NAME' )
# You can also use find_object() on other object lists:
object = find_object( 'ORIGINAL_NAME', list=OBJECT_LIST)
# To retrieve more than on object, you can use find_objects:
object_list = find_objects( 'ORIGINAL_NAME' )
# You can also use find_object() on other object lists:
object_list = find_objects( 'ORIGINAL_NAME', list=OBJECT_LIST)