Dependency System - efroemling/ballistica GitHub Wiki

NOTE: This system is largely done and will come with 1.8 release. Stay tuned.

One of the biggest changes in Ballistica as compared to the legacy BombSquad engine is the dependency system. In the old engine, game assets such as models and textures were bundled as simple files with the game and loaded by Python code at arbitrary times. This required every client connected to a game to have all necessary assets present or else the client would error out. This made the distribution of mods tedious and error-prone, especially considering different platforms can use different formats for textures or other assets.

To fix this situation, Ballistica 1.8 introduces a cloud based dependency system which allows individual game components to declare the exact assets they require, and the engine itself will then take care of resolving and downloading all missing assets before using that component. Texture formats or other platform-specific differences are handled automatically.

This does, however, require some changes to asset related code. To demonstrate, the following is an example of an Actor that simply plays a sound when created and another when destroyed.

In the old system, it might look something like this:

class SoundActor(bs.Actor):
  """An Actor that makes a sound at birth and death."""

  def __init__(self):
    self.birthSound = bs.getSound('myAwesomeAssetsSound1')
    self.deathSound = bs.getSound('myAwesomeAssetsSound2')
    bs.playSound(self.birthSound)

  def handleMessage(self, msg):
    if is instance(msg, bs.DieMessage):
      bs.playSound(self.deathSound)

This code is simple, but if any connected player had not explicitly installed the myAwesomeAssetsSound1.ogg and myAwesomeAssetsSound2.ogg files, their client would error out of the game. That's no fun.

With the new dependency system, ba.DependencyComponent classes can be used to define dynamic asset dependencies. So the same Actor might look something like this under the new system:

class SoundActor(ba.Actor):
  """An Actor that makes a sound at birth and death."""

  def __init__(self, factory: SoundActorFactory):
    self.factory = factory
    ba.playsound(self.factory.birthsound)

  def handlemessage(self, msg: Any) -> Any:
    if instance(msg, ba.DieMessage):
      ba.playsound(self.factory.deathsound)


class SoundActorFactory(ba.DependencyComponent):
  """A class we use to create SoundActors."""

  # Add a dependency at the class level.
  my_assets = ba.Dependency(ba.AssetPackage, 'my_awesome_assets@1')

  def __init__(self) -> None:
    # Access the now-loaded dependency on an instance of our class.
    self.birthsound = self.my_assets.getsound('sound1')
    self.deathsound = self.my_assets.getsound('sound2')

  def newsound(self) -> SoundActor:
    return SoundActor(factory=self)

Here we define the Actor class as well as a 'factory' class which is a ba.DependencyComponent containing its own sub-dependencies (an asset-package in this case). Note that we declare dependencies at the class level (my_assets = XXX) but use them at the instance level (YYY = self.my_assets.ZZZ). The engine will ensure all sub-dependencies of a ba.DependencyComponent have been downloaded or otherwise made available before it can be instantiated.

The one big side effect of the dependency system is that all classes that contain dependencies must inherit from DependencyComponent themselves and must be instantiated in a particular way (by being listed as a dependency on other DependencyComponent classes). This is the reason that we need to create a separate factory class above instead of adding ba.Dependencies to our actor class. Note that classes such as Games do inherit from DependencyComponent so we can add dependencies directly to those classes.

So, to continue the above example, the code to create SoundActors in a game under the old system would look something like:

class MyMiniGame(bs.TeamGameActivity):

  def onBegin(self):
    sound = myMod.SoundActor()

In the new dependency-based world it looks like this:

class MyMiniGame(ba.TeamGameActivity):
  
  # Add a dependency at the class level.
  soundfactory = ba.Dependency(mymod.SoundActorFactory)

  def on_begin(self) -> None:
    # Access the now-loaded dependency on an instance of our class.
    sound = self.soundfactory.newsound()

In a way, this is not too different from old code, since some classes such as Spaz and Bomb incorporated factory objects already; it is just more standardized and universal now.


<< Coding Style Guide        Meta Tag System >>