UnigineEditor
Interface Overview
Assets Workflow
Settings and Preferences
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Landscape Tool
Using Editor Tools for Specific Tasks
FAQ
编程
Fundamentals
Setting Up Development Environment
Usage Examples
UnigineScript
C++
UUSL (Unified UNIGINE Shader Language)
File Formats
Rebuilding the Engine and Tools
GUI
Double Precision Coordinates
应用程序接口
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
CIGI Client Plugin
Rendering-Related Classes

Using C# Component System

The C# Component System enables you to implement your application's logic via a set of building blocks — components, and assign these blocks to nodes. A logic component integrates a node and a C# class containing logic implementation.

This example demonstrates how to:

  • Decompose application logic into building blocks
  • Create your own logic components
  • Implement interaction between your components
  • Apply logic implementation to game objects

Let's make a simple game to demonstrate how the whole system works.

Game Description#

In the center of the play area, we are going to have an object (Pawn) controlled by the player via the keyboard. It has certain amount of HP and movement parameters (movement and rotation speed).

Four corners of the play area are occupied by rotating objects (Spinners) that throw other small objects (Projectiles) in all directions while rotating.

Each Projectile moves along a straight line in the directon it has been initially thrown by the Spinner. If a Projectile hits a Pawn, the Pawn takes damage according to the value set for the hitting Projectile (each of them has a random speed and damage value). The pawn is destroyed if the amount of HP drops below zero.

We use boxes for simplicity, but you can easily replace them with any other objects.

The basic workflow for implementing application logic using the Component System is given below.

1. Prepare a Project#

Before we can start creating components and implementing our game logic, we should create a project.

  1. Open the SDK Browser and create a new C# project. To do this, select C# (.NET Core) in the API + IDE list of the Application section.

  2. Run the Editor by clicking Edit Content.
  3. Create a new world.
  4. Prepare the world: delete or disable unnecessary objects, such as the ground and the material_ball.

2. Decompose Application Logic into Building Blocks#

First of all, we should decompose our application logic in terms of bulding blocks — components. So, we should define parameters for each component (all these parameters will be described in a corresponding .cs file) and decide in which functions of the execution sequence the component's logic will be implemented.

For our little game, we are going to use one component for each type of object. Thus, we need 3 components:

  • Pawn with the following parameters:
    • name — name of the Pawn
    • moving speed — how fast the Pawn moves
    • rotation speed — how fast the Pawn rotates
    • health — HP count for the Pawn

    We are going to initialize a Pawn, do something with it each frame, and report a message, when the Pawn dies. Therefore, this logic will be implemented inside the init(), update(), and shutdown() methods.

  • Spinner with the following parameters:
    • rotation speed — how fast the Spinner rotates
    • acceleration — how fast Spinner's rotation rate increases
    • node to be used as a projectile
    • minimum spawn delay
    • maximum spawn delay

    We are going to initialize a Spinner and do something with it each frame. Therefore, this logic will go to the init() and update().

  • Projectile with the following parameters:
    • speed — how fast the Projectile moves
    • life time — how long the Projectile lives
    • damage — how much damage the Projectile causes to the Pawn it hits

    As for the projectile, it will be spawned and initialized by the Spinner. The only thing we are going to do with it, is checking for a hit and controlling the life time left every frame. All of this goes to update().

3. Create a C# Component for Each Object#

For each of our objects, we should describe logic in a separate C# component. Therefore, we should do the following:

  • Create a C# component for each game entity.

  • Open the source code in the default IDE by double clicking any *.cs asset.
  • Declare all parameters defined above with their default values (if any).
  • Declare which methods we are going to use to implement our logic, and during which stages of the execution sequence to call them.
    Source code (C#)
    void Init()
    {
    	
    }
    void Update()
    {
    	
    }
    void Shutdown()
    {
    	
    }
  • Declare all necessary auxiliary parameters and functions.

Thus, for our Pawn, Spinner, and Projectile components, we will have the following classes:

Notice
You can copy this sample code and paste it to your source files, however, be careful not to change the values of the [Component(PropertyGuid = "")] attributes.

Pawn.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "6fea05debc55b3a57d79d0c0878b0d1fcdbff7f2")]
public class Pawn : Component
{
	// parameters
	public String name = "Pawn1";		// Pawn's name
	public int health = 200;			// health points
	public float move_speed = 4.0f;		// move speed (meters/s)
	public float turn_speed = 90.0f;	// turn speed (deg/s)

	// auxiliary parameters
	private Controls controls;
	private Player player;

	private float damage_effect_timer = 0.0f;
	private mat4 default_model_view;

	void Init()
	{
		// write here code to be called on component initialization
		
	}
	
	void Update()
	{
		// write here code to be called before updating each render frame
		
	}

	void Shutdown()
	{
		// write here code to be called on component shutdown
	}

	// decrease Pawn's HP 
	public void hit(int damage)
	{

	}

	// a method for damage visualization
	private void show_damage_effect()
	{

	}
}

Spinner.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "7bffd96cb49a05db58f9817ed000bac5541fed60")]
public class Spinner : Component
{

	// parameters
	public float turn_speed = 30.0f;
	public float acceleration = 5.0f;

	public Node spawn_node;
	public float min_spawn_delay = 1.0f;
	public float max_spawn_delay = 4.0f;

	private float start_turn_speed = 0.0f;
	private float color_offset = 0.0f;
	private float time_to_spawn = 0.0f;
	private Material material;

	void Init()
	{
		// write here code to be called on component initialization
		
	}
	
	void Update()
	{
		// write here code to be called before updating each render frame
		
	}

	// an auxiliary method for color convertion
	private vec3 hsv2rgb(float h, float s, float v)
	{
		return new vec3();
	}
}

Projectile.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "aaedea4a9717552184c26c0cfe8737fb811dcd1e")]
public class Projectile : Component
{
	// parameters
	public float speed = 5.0f;
	public float lifetime = 5.0f;	// life time of the projectile (declaration with a default value)
	public int damage;				// damage caused by the projectile (declaration with no default value)

	void Init()
	{
		// write here code to be called on component initialization
		
	}
	
	void Update()
	{
		// write here code to be called before updating each render frame
		
	}
}

All saved changes of the components source code make the components update with no compilation required when the Editor window gets focus.

4. Implement Each Component's Logic#

After making necessary declarations, we should implement logic for all our components.

Pawn's Logic#

The Pawn's logic is divided into the following elements:

  • Initialization — here we set necessary parameters, and the Pawn reports its name:
    Source code (C#)
    Log.Message("PAWN: INIT \"{0}\"\n", name);
  • Main loop — here we implement the player's keyboard control using methods of the Input class.
    Notice
    To access the node from the component, we can simply use node, e.g. to get the current node's direction we can write:
    Source code (C#)
    vec3 direction = node.GetWorldDirection(MathLib.AXIS.Y);
  • Shutdown — here we implement actions to be performed when a Pawn dies. We'll just print a message to the console.
  • Auxiliary — a method to be called when the pawn is hit, and some visual effects.

Implementation of the Pawn's logic is given below:

Pawn.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "6fea05debc55b3a57d79d0c0878b0d1fcdbff7f2")]
public class Pawn : Component
{
    // parameters
    public String name = "Pawn1";       // Pawn's name
    public int health = 200;            // health points
    public float move_speed = 4.0f;     // move speed (meters/s)
    public float turn_speed = 90.0f;    // turn speed (deg/s)

    // auxiliary parameters
    private Controls controls;
    private Player player;

    private float damage_effect_timer = 0.0f;
    private mat4 default_model_view;

    private const float DAMAGE_EFFECT_TIME = 0.5f;

    void Init()
    {
        player = Engine.game.Player;
        controls = player.Controls;

        default_model_view = player.Camera.Modelview;
        damage_effect_timer = 0.0f;
        show_damage_effect();

        Log.Message("PAWN: INIT \"{0}\"\n", name);
    }

    void Update()
    {
        // get delta time between frames
        float ifps = Engine.ifps;

        // show damage effect
        if (damage_effect_timer > 0)
        {
            damage_effect_timer = Math.Clamp(damage_effect_timer - ifps, 0.0f, DAMAGE_EFFECT_TIME);
            show_damage_effect();
        }

        // if console is opened we disable any controls
        if (Engine.console.Activity)
            return;

        // get the forward direction vector of the node
		vec3 direction = node.GetWorldDirection(MathLib.AXIS.Y);

        // checking controls states and changing pawn position and rotation
        if (Engine.input.IsKeyPressed(Input.KEY.UP) || Engine.input.IsKeyPressed(Input.KEY.W))
        {
            node.WorldPosition += direction * move_speed * ifps;
        }

        if (Engine.input.IsKeyPressed(Input.KEY.DOWN) || Engine.input.IsKeyPressed(Input.KEY.S))
        {
            node.WorldPosition -= direction * move_speed * ifps;
        }
		
        if (Engine.input.IsKeyPressed(Input.KEY.LEFT) || Engine.input.IsKeyPressed(Input.KEY.A))
        {
            node.Rotate(0.0f, 0.0f, turn_speed * ifps);
        }
		
        if (Engine.input.IsKeyPressed(Input.KEY.RIGHT) || Engine.input.IsKeyPressed(Input.KEY.D))
        {
            node.Rotate(0.0f, 0.0f, -turn_speed * ifps);
        }
    }

    void Shutdown()
    {
        Log.Message("PAWN: DEAD!\n");
    }

    // method to be called when the Pawn is hit by a Projectile
    public void hit(int damage)
    {
        // take damage
        health = health - damage;

        // show effect
        damage_effect_timer = DAMAGE_EFFECT_TIME;
        show_damage_effect();

        Log.Message("PAWN: damage taken ({0}) - HP left ({1})\n", damage, health);

        // destroy
        if (health <= 0)
		{
            health = 0;
            Engine.world.RemoveNode(node);
		}

    }

    private void show_damage_effect()
    {
        float strength = damage_effect_timer / DAMAGE_EFFECT_TIME;
        Engine.render.FadeColor = new vec4(0.5f, 0, 0, MathLib.Saturate(strength - 0.5f));
        player.Camera.Modelview = default_model_view * new mat4(
            MathLib.RotateX(Engine.game.GetRandomFloat(-5, 5) * strength) *
            MathLib.RotateY(Engine.game.GetRandomFloat(-5, 5) * strength));
    }
}

Projectile's Logic#

The Projectile's logic is simpler — we just have to perform a check each frame whether we hit the Pawn or not. This means that we have to access a Pawn component from the Projectile component.

Notice
To access certain component on a certain node (e.g. the one that was intersected in our case) we can use the ComponentSystem's GetComponent<T>() or the Node's GetComponent<T>() functions:
Source code (C++)
// get the component assigned to a node by type "MyComponent"
MyComponent my_component = GetComponent<MyComponent>(some_node);

// do the same by using the function of node
my_component = some_node.GetComponent<MyComponent>();

// access some method of MyComponent
my_component.someMyComponentMethod();

The Projectile has a limited life time, so we should destroy the node when its life time is expired. Use the RemoveNode function of the World class for this purpose.

Source code (C#)
Engine.world.RemoveNode(node);

Implementation of the Projectile's logic is given below:

Projectile.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "aaedea4a9717552184c26c0cfe8737fb811dcd1e")]
public class Projectile : Component
{
	// parameters
    public float speed = 5.0f;
    public float lifetime = 5.0f;   // life time of the projectile (declaration with a default value)
    public int damage;              // damage caused by the projectile (declaration with no default value)

    void Update()
    {
        // get delta time between frames
        float ifps = Engine.ifps;

        // get the forward direction vector of the node
		vec3 direction = node.GetWorldDirection(MathLib.AXIS.Y);

        // move forward
        node.WorldPosition +=  direction * speed * ifps;

        // lifetime
        lifetime = lifetime - ifps;
        if (lifetime < 0)
        {
            // destroy current node with its properties and components
            Engine.world.RemoveNode(node);
            return;
        }

        // check the intersection with nodes
        List<Node> nodes = new List<Node>();
        Engine.world.GetIntersection(node.WorldBoundBox, nodes);
        if (nodes.Count > 1) // (because the current node is also in this list)
        {
            foreach (Node n in nodes)
            {
                Pawn pawn = n.GetComponent<Pawn>();

                if (pawn != null)
                {
                    // hit the player!
                    pawn.hit(damage);

                    // ...and destroy the current node
                    Engine.world.RemoveNode(node);
                    return;
                }
            }
        }
    }

    public void setMaterial(Material mat)
	{
		Unigine.Object.cast(node).SetMaterial(mat, "*");
	}
}

Spinner's Logic#

The Spinner's logic is divided into the following elements:

  • Initialization — here we set necessary parameters to be used in the main loop.
  • Main loop — here we rotate our Spinner and spawn nodes with Projectile components. We also set some parameters of the Projectile.

    You can change variables of another component directly:

    Source code (C#)
    component.int_parameter += 1;
  • Auxiliary — color conversion function.

Implementation of the Spinner's logic is given below:

Spinner.cs

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "7bffd96cb49a05db58f9817ed000bac5541fed60")]
public class Spinner : Component
{
    // parameters
    public float turn_speed = 30.0f;
    public float acceleration = 5.0f;

    public Node spawn_node;
    public float min_spawn_delay = 1.0f;
    public float max_spawn_delay = 4.0f;

    private float start_turn_speed = 0.0f;
    private float color_offset = 0.0f;
    private float time_to_spawn = 0.0f;
    private Material material;

    void Init()
    {
        // get current material (from the first surface)
        Unigine.Object obj = Unigine.Object.cast(node);
        if (obj != null && obj.NumSurfaces > 0)
            material = obj.GetMaterialInherit(0);

        // init randoms
        time_to_spawn = Engine.game.GetRandomFloat(min_spawn_delay, max_spawn_delay);
        color_offset = Engine.game.GetRandomFloat(0, 360.0f);
        start_turn_speed = turn_speed;
    }

    void Update()
    {
        // rotate spinner
        float ifps = Engine.ifps;
        turn_speed = turn_speed + acceleration * ifps;
        node.Rotate(0.0f, 0.0f, turn_speed * ifps);

        // change color
        int id = material.FetchParameter("albedo_color", 0);
        if (id != -1)
        {
            float hue = MathLib.Mod(Engine.game.Time * 60.0f + color_offset, 360.0f);
            material.SetParameter(id, new vec4(hsv2rgb(hue, 1, 1), 1.0f));
        }

        // spawn projectiles
        time_to_spawn -= ifps;
        if (time_to_spawn < 0 && spawn_node != null)
        {
            // reset timer and increase difficulty
            time_to_spawn = Engine.game.GetRandomFloat(min_spawn_delay, max_spawn_delay) / (turn_speed / start_turn_speed);

            // create node
            Node spawned = spawn_node.Clone();
            spawned.Enabled = true;
            spawned.WorldTransform = node.WorldTransform;

            // create component
            Projectile proj_component = AddComponent<Projectile>(spawned);

            // change variables inside another component
            proj_component.speed = Engine.game.GetRandomFloat(proj_component.speed * 0.5f, proj_component.speed * 1.5f);
            proj_component.damage = Engine.game.GetRandomInt(75, 100);
            proj_component.lifetime = Engine.game.GetRandomInt(75, 100);

            // call public method of another component
            proj_component.setMaterial(material);
        }
    }

	// color conversion H - [0, 360), S,V - [0, 1]
    private vec3 hsv2rgb(float h, float s, float v)
    {
        float p, q, t, fract;

        h /= 60.0f;
        fract = h - MathLib.Floor(h);

        p = v * (1.0f - s);
        q = v * (1.0f - s * fract);
        t = v * (1.0f - s * (1.0f - fract));

        if (0.0f <= h && h < 1.0f) return new vec3(v, t, p);
        else if (1.0f <= h && h < 2.0f) return new vec3(q, v, p);
        else if (2.0f <= h && h < 3.0f) return new vec3(p, v, t);
        else if (3.0f <= h && h < 4.0f) return new vec3(p, q, v);
        else if (4.0f <= h && h < 5.0f) return new vec3(t, p, v);
        else if (5.0f <= h && h < 6.0f) return new vec3(v, p, q);
        else return new vec3(0, 0, 0);
    }
}

5. Add Components to Nodes#

As we implemented our game logic in the components, we can actually start using them. But first, we should create game objects. It is possible both via UnigineEditor and code.

Creating a Scene via Editor#

  1. Create a World light source to illuminate the whole scene.
  2. Add a new camera to the world and adjust its position. This will be our default camera, so check on its Main Player flag.
  3. Create a Box primitive to represent the pawn. Assign the Pawn component by clicking Add New Property and dragging the asset to the corresponding field. You can also drag the component from the Asset Browser to the node in the Scene viewport. Adjust the parameters of the player.

  4. Create another smaller box — a template for projectiles. We can disable it since its clones will be used.
  5. Create objects for spinners at the same height as the pawn, assign the corresponding component to them and adjust the parameters. Specify the spawn node by dragging the projectile node to the field.

    On this step the scene looks as follows:

Creating a Scene via Code#

All the same is available via API. We can describe logic to be executed on world loading (create game objects and assign components to them) in the AppWorldLogic class.

There are two ways to add a logic component to a node:

  • By calling the corresponding method of the Node class:
    Source code (C#)
    object.AddComponent<MyComponent>();
  • By calling the corresponding method of the Component System:
    Source code (C#)
    ComponentSystem.AddComponent<MyComponent>(object.getNode());

Here is the resulting code for our game including creation of entities and adding components to them:

AppWorldLogic.cs

Source code (C#)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Unigine;

namespace UnigineApp
{
    class AppWorldLogic : WorldLogic
    {
        Pawn my_pawn;   // Pawn player
        float time = 0;
        WidgetLabel label;
        PlayerDummy player;
        LightWorld sun;

        // method creating a box
        ObjectMeshDynamic create_box(mat4 transform, vec3 size)
        {
            Mesh mesh = new Mesh();
            mesh.AddBoxSurface("box", size);
            ObjectMeshDynamic obj = new ObjectMeshDynamic(1);
            obj.SetMesh(mesh);
            obj.WorldTransform = transform;
            obj.SetMaterial("mesh_base", "*");

            return obj;
        }

        public override bool Init()
        {
            // create static camera
            player = new PlayerDummy();
            player.Position = new vec3(17.0f);
            player.SetDirection(new vec3(-1.0f), new vec3(0.0f, 0.0f, 1.0f));
			player.Controlled = true;
            Engine.game.Player = player.GetPlayer();

            // create light
            sun = new LightWorld(vec4.ONE);
            sun.Name = "Sun";
            sun.WorldRotation = new quat(45.0f, 30.0f, 300.0f);

            // create objects
            ObjectMeshDynamic[] obj = new ObjectMeshDynamic[4];
            obj[0] = create_box(MathLib.Translate(new vec3(-16.0f, 0.0f, 0.0f)), new vec3(1.0f));
            obj[1] = create_box(MathLib.Translate(new vec3(16.0f, 0.0f, 0.0f)), new vec3(1.0f));
            obj[2] = create_box(MathLib.Translate(new vec3(0.0f, -16.0f, 0.0f)), new vec3(1.0f));
            obj[3] = create_box(MathLib.Translate(new vec3(0.0f, 16.0f, 0.0f)), new vec3(1.0f));

            // there are two ways to create components on nodes:
            ComponentSystem.AddComponent<Spinner>(obj[0].GetNode());
            ComponentSystem.AddComponent<Spinner>(obj[1].GetNode());
            obj[2].AddComponent<Spinner>();
            obj[3].AddComponent<Spinner>();

            // set up spinners (set "spawn_node" variable)
            ObjectMeshDynamic projectile_obj = create_box(mat4.IDENTITY, new vec3(0.15f));
            projectile_obj.Enabled = false;
            for (int i = 0; i < 4; i++)
                obj[i].GetComponent<Spinner>().spawn_node = projectile_obj;

            // create player
            ObjectMeshDynamic my_pawn_object = create_box(MathLib.Translate(new vec3(1.0f, 1.0f, 0.0f)), new vec3(1.3f, 1.3f, 0.3f));
            my_pawn = my_pawn_object.AddComponent<Pawn>();
            
            time = 0;

            // create info label
            label = new WidgetLabel(Gui.get());
            label.SetPosition(10, 10);
            label.FontSize = 24;
            label.FontOutline = 1;
            Engine.gui.AddChild(label.GetWidget(), Gui.ALIGN_OVERLAP);

            return true;
        }

        public override bool Update()
        {
            // increase time while player is alive
            if (my_pawn != null)
                time += Engine.ifps;
            // show game info
            label.Text = 
                "Player:\n" +
                "Health Points: " +
                (my_pawn != null ? my_pawn.health : 0) + "\n" +
                "Time: " + time + " sec\n";
                
            return true;
        }
    }
}

Return to the Editor.

Running the Project#

Select a desired play preset on the toolbar and click the Play button to run an instance of the application.

Last update: 2019-07-18