Introduction
Hola Pingheros! 👋
Romain here, I want to talk to you about an architectural concept that use in Los Pingheros:
AssetActions. If the term looks pretty generic to you, it is because it is supposed to be,
AssetActions are meant to represent a wide variety of things that can be done in our game. Let’s get into it.
In case you don’t know it already,
LosPingheros is a top down snowball fighting game where a lot of crazy stuff can happen: explosions, sharks chasing you, ananas meteor sent from the penguins gods, etc.
Thus, we developed a very systemic game and as we added features, the combinatorics of associating the different incomes with the different outcomes would have quickly exploded, se we searched for a generic solution to the problem.
[h2]What is an AssetAction[/h2]
We have different kind of triggers in our game, and the goal is to unify the way we produce outcome from those triggers. We need to be able to:
- Describe the outcome of the interaction
- Prototype things easily
- Give the designers the tools to leverage the systemic aspect of the game

Here is an example of what it looks like in the editor. Our coins are
Interactable objects that trigger an action when the player touch them. The outcome of this interaction is given by the
AssetAction associated to the
Interactable component.
AssetActions are scriptable objects (more precisely
AssetObject as we are using Photon Quantum, but this is not the topic) that implements the
IAssetAction interface.
internal interface IAssetAction
{
unsafe void OnAssetAction(Frame f, AssetActionRuntimeData context);
}
Simple and effective, the scriptable object must describe what happens when it executes, adding score to the player that triggered the action.
public unsafe partial class ScoreActionConfig : AssetObject, IAssetAction, IAssetInteraction
{
public ScoringValue ScoringValue;
public void OnAssetAction(Frame f, AssetActionRuntimeData context)
{
// Add score to the associated player
var player = f.C.pPlayerData->Get(f, context.Owner);
player->AddScore(ScoringValue);
// Send an event to the view to create FXs
ObservableEvent.Send(f,
position: context.Position,
owner: context.Owner);
}
}

We previously, created an explosive bomb (is there any other kind of bomb really ?) as you might have seen here:
VFX Creation, and the use of symbols. So what happens if we just swap the AssetAction from the score one to the explosive one ?

We just gave a way for the designers to add a fake explosive coin without any extra development cost ! And this is just the beginning…
[h2]AssetActionRuntimeData[/h2]
You might have noticed in the
IAssetAction description that a
AssetActionRuntimeData is passed as a parameter of the function. This truly has 3 purposes:
Pass other possibly meaningful parameters to the Action:This is pretty straightforward in the example, it gives information on the context of the interaction that triggered the AssetAction, such as the position, the frame, the player, etc.
Give the possibility to execute the action at another point in the frame:You don’t always want to execute the action instantly, might it be for logical reasons or performances. For example, you might not want to apply scores before you gathered everything that triggered score in the frame. This allows you to handle things in batch.
This is what the
IAssetInteraction interface is for in the
ScoreActionConfig. This interface is completely empty, and serves as a tag to tell when the action is supposed to happen.
switch (assetObject)
{
...
case IAssetInteraction:
AssetActionRuntimeData.CreateAssetAction(f, in assetAction);
//Executed later in the frame
break;
case IAssetAction config:
config.OnAssetAction(f, assetAction);
// Executed instantly
break;
...
The system handling
AssetActions just check for the implementations of the AssetAction and execute different logic based on it.
IAssetInteraction are added to an interaction queue to be handled later on, while basic
IAssetActions are executed instantly.
Give the possibility to delay the action more in the future:Sometime, you don’t event want the action to happen in the frame where it was triggered, in that case, you just store
AssetActionRuntimeData somewhere and wait for it to be ready to be executed.
[h2]Different kind of AssetActions[/h2]
Here is a sample of the kind of
AssetActions you can find in our game:
AOEConfig
Create an area of effect applying different kind impacts to objects in the area. There is different shapes, different elements, different propagation logic, all defined as variables in the scriptable object.
SharkConfig
Modify the AI of the shark to make him try to bite a certain location or a certain player.
CharacterStatModConfig
Apply a buff to a player, making him as jumpy as a rockhopper or as strong as an emperor.
ActionOverrideConfig
Apply some modifiers to basic actions of the character, making them fly instead of jumping for example.
JumpAction (CustomAction)
Make an object jump, even if it is not supposed to. There is a bunch of actions like that for things that are not really factorizable.
MeteorShowerConfig
Well… Create a meteor shower… of pineapples !
SpawnItemConfig
Spawn a prefab at a certain place
TweakExplodableConfig
Modify an explodable, increasing its range for example.
And the list goes on. This is really powerful and might even unlock a new level of creativity for your designers. Would they have thought of a bomb which increase the range of other bombs in the area when it explodes ?
We also have a way to describe aggregated actions in a single one, would you trigger several things at the same time, or randomly pick one in a table.
public partial class RandomActionListConfig : AssetObject, IAssetAction
{
[Serializable]
public struct WeightedAction
{
public AssetRef AssetAction;
public FP Weight;
}
public List WeightedActions;
public unsafe void OnAssetAction(Frame f, AssetActionRuntimeData context)
{
// Compute total weight
FP cumulatedWeight = 0;
foreach (var action in WeightedActions)
{
cumulatedWeight += action.Weight;
}
// Random value
var pGameSessionState = f.Unsafe.GetPointerSingleton();
var randFP = pGameSessionState->SystemRandoms.AssetAction.Next(0, cumulatedWeight);
// Find random action
cumulatedWeight = 0;
foreach (var action in WeightedActions)
{
cumulatedWeight += action.Weight;
if (cumulatedWeight > randFP)
{
context.AssetActionContext.AssetRef = action.AssetAction;
f.Signals.OnAssetAction(context);
break;
}
}
}
}
Conclusion
This is a simple overview of what you can achieve with externalizing some logic in scriptable objects. Making systems like this help you go further faster, decoupling coding the interaction from using them, and even helping the creativity of the design of systemic games. How powerful !
But… with great power… comes great responsibilities… Don’t let your designers go crazy.

Don’t freeze your brains up ‘til next time, see you on ice !
Romain GROSSE