Rule Assembly

Rule assembly is the process of generating scripts. It occurs when product build is called, and also when saving a rule to validate C# syntax. Rule assembly recursively assemble all used sub rules using rule resolution mechanism to find best match rule instance.

Partial rule assembly can be previewed for any rule from Action > View C# menu:

_images/viewcsharpmenu.png

Caution

This preview is a partial C# generation and can differ from actual generated script. Generated scripts are assembled from a specific primary class for flows and state machines.

Let’s take a simple flow as an example to walkthrough the rule assembly mechanism. This example can be easily extended to any kind of flow or state machine.

The flow used as an example is inspired from Tanks tutorial from Unity. ShellExplosion is used to manage the explosion of a shell when it touches a tank and calls a public method TakeDamage from a tank script. ShellExplosion flow is in class Tanks-Shell, in ruleset Tanks.

_images/ruleassemblyflowexample.png

This flow has here two start shapes:

  • Start: calls the message rule Start
  • OnTriggerEnter: calls the message rule OnTriggerEnter

During rule assembly, the flow rule assembler will start from start shapes to assemble recursively sub-rules.

Assembly step 1

The very first step for rule assembly is to find the best match flow rule ShellExplosion. In our example, as the primary class is the same than the flow class (Tanks-Shell) and it is our first game release and there is no other version, it will find only one instance of this rule. Once opened from database, the rule assembly loops over different start shapes.

The first start shape to assemble is Start:

_images/ruleassemblystep1.png

It references the message rule Start. The rule resolution finds only one instance of rule message Start in following class and ruleset:

  • class: Core
  • ruleset: GameRulesCore:1
  • rule: Start
_images/ruleassemblystart.png

This is a Unity Monobehaviour message. The message rule assembly of Start generates following code:

void Start ( )
{

Assembly step 2

Continuing with Start start shape, the next shape is Destroy. It is a logic shape calling the logic DestroyGameObject.

_images/ruleassemblystep2.png

We can see that the input parameters refers to gameObject, which is an inherited variable from Monobehaviour, and the property rule MaxLifeTime.

Hint

the property rule is referenced with prefix @. Because it is automatically added as a public variable to the script, you don’t need to declare it elsewhere.

Before assembling the logic, rule assembler first assemble the property rule MaxLifeTime. Using rule resolution from Tanks-Shell, it finds following instance:

  • class: Tanks-Shell
  • ruleset: Tanks:1
  • rule: MaxLifeTime
_images/ruleassemblymaxlifetime.png

and adds a public float variable to the global script:

public float MaxLifeTime = 2f;

Once the property rule is assembled, it can start the assembly of the logic DestroyGameObject. This logic is coming from GameRules Core layer:

  • class: Core
  • ruleset: GameRulesCore:1
  • rule: DestroyGameObject
_images/ruleassemblydestroy.png

This logic just destroys a game object after a certain delay. A logic is generated inside a specific method named perform_<logic name>:

void perform_DestroyGameObject (GameObject aGameObject, float delay)
{
        // Code to call step 1
        Destroy(aGameObject, delay);
}

And lastly, the call to the logic method can be added to Start method:

void Start ( )
{
        // Logic State Destroy
        perform_DestroyGameObject(gameObject, MaxLifeTime);
}

Important

In the flow, the property rule specified was @MaxLifeTime, and during rule assembly the variable used is MaxLifeTime (without the prefix @).

Note

The last shape is end which does nothing as the start shape message returns void. This ends the rule assembly of Start start shape and appends a closing bracket to Start method.

Assembly step 3

The second start shape is OnTriggerEnter. One again, it refers to the message rule OnTriggerEnter from GameRules Core layer, representing a Unity Monobehaviour standard method:

_images/ruleassemblystep3.png

The OnTriggerEnter message rule defines a parameter Collider otherCollider. The generated code is:

void OnTriggerEnter (Collider otherCollider)
{

Hint

this otherCollider variable can then be used as input parameter for consecutive logics.

Assembly step 4

After OnTriggerEnter, we have a logic shape which calls the logic FindTanksAndDamageThem. This logic is very specific to shells. That’s why it is created directly under class Tanks-Shell.

_images/ruleassemblystep4.png

The logic FindTanksAndDamageThem is doing different steps. It first get current colliders around. Then, in step 2, it loops over colliders to call sub-logic CalculateDamage.

_images/ruleassemblyfindtanks.png

Each time a new property rule is called, it is assembled in same time than this logic and added as global variable to the script. For example, in first step, you can find references to properties ExplosionRadius and TankMask from property-set. Variable colliders is local variable to this logic of class Collider[].

_images/ruleassemblyfindtanksstep1.png

Rule resolution and therefore rule assembly is executed for property rules ExplosionRadius and TankMask. Resulting code after step 1 is:

public float ExplosionRadius = 5f;

public LayerMask TankMask;


void OnTriggerEnter (Collider otherCollider)
{
        // Logic State FindTanksAndDamageThem
        perform_FindTanksAndDamageThem();
}

void perform_FindTanksAndDamageThem ()
{
        Collider[] colliders;

        // Code to call step 1
        // get all colliders around
        colliders = Physics.OverlapSphere(transform.position, ExplosionRadius, TankMask);

For step 2 of the logic, we have a C-Sharp text area with manual C# code. It is used to be able to loop over the list of colliders present inside the sphere. Now, to make the code reusable and maintainable, instead of writing C# directly inside this text area, we just call another logic CalculateDamage which takes a collider as input parameter.

_images/ruleassemblyfindtanksstep2.png

Hint

as we are in a C# text area, we have to use the generated name of the logic method (perform_CalculateDamage). Because rule assembly will not detect automatically the call to this logic, we have also to add manually this logic to the Additional logics list under Parameters tab.

Recursively, rule assembly will open CalculateDamage logic rule and assemble it to current script. After step 2, the added generated code looks like:

public float MaxLifeTime = 2f;

public float ExplosionRadius = 5f;

public LayerMask TankMask;

public ParticleSystem ExplosionParticles;

public AudioSource ExplosionAudio;

public float ExplosionForce = 1000f;

public float MaxDamage = 100f;

void perform_CalculateDamage (Collider collider)
{
        // code generated for logic CalculateDamage and referenced property and when rules.
        [...]
}

void perform_FindTanksAndDamageThem ()
{
        Collider[] colliders;

        // Code to call step 1
        // get all colliders around
        colliders = Physics.OverlapSphere(transform.position, ExplosionRadius, TankMask);

        // Code to call step 2
        // loop over colliders to apply damages
        for(int i=0; i<colliders.Length; i++)
        {
                perform_CalculateDamage(colliders[i]);
        }

The other steps are then assembled the same way.

Hint

when rules are generated as method bool evaluate_<when rule>()

Assembly wrap-up

Once all shapes from flow, all referenced rules (message, property, logic and when rules) are assembled, the C# code is saved to following folder:

  • Assets/GameRulesWorkspace/Scripts/Tanks/Shell/ShellExplosion_Tanks_Shell_RuleObjFlow.cs

Important

The folder structure is the same than primary class defined in product rule. The name of the class and of the C# script contains also the primary class where ‘-‘ characters are replaced by ‘_’.

The created script inherits from Monobehaviour and you are able to immediately attach it to a gane object in your scene without any modification. For each change in your rules, in exiting ruleset version of with new specialized rules, you just have to rebuild your product and this script will get automatically rebuilt and regenerated.

Hint

the namespace imports are the aggregation of the library rules referenced in application rule in Library list configuration. Each library rule defines a set of namespaces to import. In this example, we are using only UnityEngine library rule.

Below is the resulting C# code for ShellExplosion flow:

using GameRules.Tracer;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

namespace GameRules
{
        public class ShellExplosion_Tanks_Shell_RuleObjFlow : MonoBehaviour
        {
                public float MaxLifeTime = 2f;

                public float ExplosionRadius = 5f;

                public LayerMask TankMask;

                public ParticleSystem ExplosionParticles;

                public AudioSource ExplosionAudio;

                public float ExplosionForce = 1000f;

                public float MaxDamage = 100f;

                // Start is called on the frame when a script is enabled just before any of the Update methods is called the first time.
                void Start ( )
                {
                        // Logic State Destroy
                        perform_Destroy(gameObject, MaxLifeTime);
                }

                void OnTriggerEnter (Collider otherCollider)
                {
                        // Logic State FindTanksAndDamageThem
                        perform_FindTanksAndDamageThem();
                }

                void perform_CalculateDamage (Collider collider)
                {
                        Rigidbody targetRigidbody;
                        float damage;
                        Vector3 targetToPosition;
                        float explosionDistance;
                        float relativeDistance;
                        bool doStep = true;
                        bool doWhenProcessing;

                        // Code to call step 1
                        targetRigidbody = collider.GetComponent<Rigidbody>();

                        // Code to call step 2
                        // preconditions
                        doWhenProcessing = true;
                        doStep = true;
                        bool pz__w2_1 = (!targetRigidbody);
                        if(!pz__w2_1){
                                doStep = false;
                        }
                        if(doStep){
                                return;
                        }

                        // Code to call step 3
                        targetRigidbody.AddExplosionForce(ExplosionForce, transform.position, ExplosionRadius);

                        // Code to call step 4
                        targetToPosition = collider.transform.position - transform.position;
                        explosionDistance = targetToPosition.magnitude;
                        relativeDistance = (ExplosionRadius - explosionDistance) / ExplosionRadius;
                        damage = relativeDistance * MaxDamage;
                        damage = Mathf.Max(0f, damage);

                        // Code to call step 5
                        if(collider.gameObject.GetComponent<TankHealth_Tanks_Tank_RuleObjFlow>() != null) collider.gameObject.GetComponent<TankHealth_Tanks_Tank_RuleObjFlow>().TakeDamage(damage);
                }

                void perform_Destroy (GameObject aGameObject, float delay)
                {
                        // Code to call step 1
                        Destroy(aGameObject, delay);
                }

                void perform_FindTanksAndDamageThem ()
                {
                        Collider[] colliders;

                        // Code to call step 1
                        // get all colliders around
                        colliders = Physics.OverlapSphere(transform.position, ExplosionRadius, TankMask);

                        // Code to call step 2
                        // loop over colliders to apply damages
                        for(int i=0; i<colliders.Length; i++)
                        {
                                perform_CalculateDamage(colliders[i]);
                        }

                        // Code to call step 3
                        // make particles independant from this shell
                        ExplosionParticles.transform.parent = null;

                        // Code to call step 4
                        // play explosion
                        ExplosionParticles.Play();

                        // Code to call step 5
                        // play explosion audio
                        ExplosionAudio.Play();

                        // Code to call step 6
                        // destroy particles
                        perform_Destroy(ExplosionParticles.gameObject, ExplosionParticles.duration);

                        // Code to call step 7
                        // destroy this shell
                        perform_Destroy(gameObject, 0f);
                }
        }
}

Caution

The logic CalculateDamage in step 5 is referencing an external flow TankHealth, calling the public message TakeDamage from game object collider.gameObject. It is expected to have also TankHealth flow part of the product build or already existing in Unity.

Step 5 in logic CalculateDamage is:

_images/ruleassemblycalculatedamagestep5.png

and generated code is:

// Code to call step 5
if(collider.gameObject.GetComponent<TankHealth_Tanks_Tank_RuleObjFlow>() != null)
        collider.gameObject.GetComponent<TankHealth_Tanks_Tank_RuleObjFlow>().TakeDamage(damage);