6 min read

Introduction

In the first part of this tutorial, we learned how static tilemaps are created in the Duality game engine. Let’s now take it to a higher level. In this article, the process of implementing a custom component is described. The new component modifies the tilemap at runtime, controlled by mouse clicks. Although it is a simple example, the method enables more advanced uses, such as procedural generation of game levels or destructible terrain.

The repository containing the finished project is available on GitHub.

Creating Custom Components

It has been already mentioned that all functionality in Duality is implemented via plugins. To create a custom component, you have to compile a new Core Plugin. Although it might seem convoluted at first, Duality does most of the work and setup, so let’s dive in!

First, open the Visual Studio Solution by clicking on the ‘Open Sourcecode’ icon button, which is the second one on the toolbar. Alternatively, just open the ProjectPlugins.sln file located in ‘{Duality Project Dir}’.

Once the IDE is open, inspect the sole loaded project called ‘CorePlugin’. It has two source files (not counting AssemblyInfo.cs), and references to the Duality assembly. CorePlugin.cs contains a class inherited from CorePlugin. It’s necessary to have this class in the solution to identify the assembly as a plugin, but usually it does not need to be modified, because game logic is implemented via custom components in most of the time.

Let’s have a look at the other class located in ‘YourCustomComponentType.cs’:

using System;
using System.Collections.Generic;
using System.Linq;

using Duality;

namespace Duality_
{
    public class YourCustomComponentType : Component
    {

    }
}

There are a few important things to notice here. The custom component must be a subclass of Component. It has to be declared public; otherwise the new component wouldn’t appear in the editor.

Don’t modify the code for now, but hit F7 to compile the assembly. Behind the scenes, the output assembly named ‘GamePlugin.core.dll’ (along with the debug symbol file and the xml documentation) is copied to ‘{Duality Project Dir}’, and Dualitor loads them. The new component is available for being added to the game, like any other component:

Adding the custom component

At this point, the new component could be added to GameObjects, but it would not do anything yet. The next sections are about how to implement the game logic in the component.

Laying the structure of ChangeTilesetCmp

Adding a reference of the Tilemap Plugin to the VS project

To access tilemap-related classes and information in the component, the Visual Studio project must have a reference to the assembly containing the Tilemap Plugin. Bring up the context menu on the ‘CorePlugin’ project’s ‘References’ item in the Solution Explorer, and select the ‘Add Reference’ item. A dialog should appear; browse ‘Tilemaps.core.dll’ from the ‘{Duality Project Dir}’.

Adding the Tilemap Plugin as a reference

Defining the internal structure of ChangeTilemapCmp

Rename the YourCustomComponentType class to ChangeTilemapCmp, and do the same with the container .cs file to stay consistent. First describe the function signatures used in the component:

using Duality;
using Duality.Components;
using Duality.Editor;
using Duality.Input;
using Duality.Plugins.Tilemaps;

namespace Duality_
    [EditorHintCategory ("Tilemaps Tutorial")] // [1]
    public class ChangeTilemapCmp : Component, ICmpUpdatable
    {
        // [2]
        private Tilemap TilemapInScene { get; }
        private TilemapRenderer TilemapRendererInScene { get; }
        private Camera MainCamera { get; }

        void ICmpUpdatable.OnUpdate () // [3]
        {
        }

        private Vector2 GetWorldCoordOfMouse () // [4]
        {
        }

        private void ChangeTilemap (Vector2 worldPos) // [5]
        {
        }
    }
}
  1. The EditorHintCategory attribute describes in which folder the component should appear when adding it.
  2. Several get-only properties are used to access specific components in the scene. See their implementation below.
  3. The OnUpdate function implements the ICmpUpdatable interface. It’s called upon every update of the game loop.
  4. GetWorldCoordOfMouse returns the current position of the mouse transformed into the game world coordinates.
  5. ChangeTilemap, as its name suggests, changes the tilemap at a specified world location. It should not do anything if the location is not on the tilemap.

After designing our little class, let’s implement the details!

Implementing the game logic in ChangeTilemapCmp

Implementing the get-only properties

The Tilemap and TilemapRenderer components are obviously needed by our logic. Since there is only one instance of them in the scene, it’s easy to find them by type:

private Tilemap         TilemapInScene         => this.GameObj.ParentScene.FindComponent<Tilemap> ();
private TilemapRenderer TilemapRendererInScene => this.GameObj.ParentScene.FindComponent<TilemapRenderer>();
private Camera          MainCamera             => this.GameObj.ParentScene.FindComponent<Camera> ();

Implementing the OnUpdate method

This method is called for every frame, usually 60 times a second. We check if the left mouse button was pressed in that frame, and if yes, act accordingly:

void ICmpUpdatable.OnUpdate ()
{
    if (DualityApp.Mouse.ButtonHit (MouseButton.Left))
        ChangeTilemap (GetWorldCoordOfMouse ());
}

Implementing the GetWorldCoordOfMouse method

Here a simple transformation is needed. The DualityApp.Mouse.Pos property returns the mouse position on the screen. After a null-check, get the mouse position on the screen; then convert it to world position using the Camera’s GetSpaceCoord method.

private Vector2 GetWorldCoordOfMouse ()
{
    if (MainCamera == null)
        return Vector2.Zero;

    Vector2 mouseScreenPos = DualityApp.Mouse.Pos;
    return MainCamera.GetSpaceCoord (mouseScreenPos).Xy;
}

Implementing the ChangeTilemap method

The main logic of ChangeTilemapCmp is implemented in this method:

private void ChangeTilemap (Vector2 worldPos)
{
    // [1]
    Tilemap tilemap = TilemapInScene;
    TilemapRenderer tilemapRenderer = TilemapRendererInScene;
    if (tilemap == null || tilemapRenderer == null) {
        Log.Game.WriteError("There are no tilemaps in the current scene!");
        return;
    }

    // [2]
    Vector2 localPos = worldPos - tilemapRenderer.GameObj.Transform.Pos.Xy;
    // [3]
    Point2 tilePos = tilemapRenderer.GetTileAtLocalPos (localPos, TilePickMode.Reject);
    if (tilePos.X < 0 || tilePos.Y < 0)
        return;

    // [4]
    Tile clickedTile = tilemap.Tiles[tilePos.X, tilePos.Y];
    int newTileIndex = clickedTile.BaseIndex == 0 ? 1 : 0;
    clickedTile.BaseIndex = newTileIndex;
    tilemap.SetTile(tilePos.X, tilePos.Y, clickedTile);
}
  1. First check if there is any Tilemap and TilemapRenderer in the Scene.
  2. Transform the world position to the tilemap’s local coordinate system by subtracting the tilemap’s position from it.
  3. Acquire the indexed location of the clicked tile by the GetTileAtLocalPos method of the renderer. It returns a Point2, an integer-based vector, which represents the row and column of the tile in the tilemap. Because TilePickMode.Reject is passed as the second argument, it returns {-1, -1} when the ‘player’ clicks next to the tilemap. We should check that too, and return if that is the case.
  4. Get the Tile struct at the clicked position, and flip its tile index. Index 0 means the green tile, while index 1 refers to the red one. Afterwards, the tile has to be assigned back to the Tilemap, because in C#, structs are copied by value, not by reference.

After finishing this, do not forget to compile the source again.

Adding and testing ChangeTilemapCmp

Create a new GameObject with ChangeTilemapCmp on it in the Scene View:

Adding ChangeTilemapCmp

Then save the scene with the floppy icon of the Scene View, and start the game by clicking the ‘Run Game’ icon button located on the toolbar. Test the game by clicking on the tilemap; it should change color on the tile you click.

Running the game

Summary

Thank you for following along with this guide! Hopefully it helped you with the Duality game engine and its Tilemap Plugin. Remember that the above was a very simple example, but more sophisticated logic is achievable using these tools. If you want some inspiration, have a look at the entries of the Duality Tilemaps Jam, which took place in September, 2016.

Author

Lőrinc Serfőző is a software engineer at Graphisoft, the company behind the BIM solution ArchiCAD. He is studying mechatronics engineering at the Budapest University of Technology and Economics, an interdisciplinary field between the more traditional mechanical engineering, electrical engineering and informatics, and has quickly grown a passion toward software development. He is a supporter of open source and contributes to the C# and OpenGL-based Duality game engine, creating free plugins and tools for users.

LEAVE A REPLY

Please enter your comment!
Please enter your name here