Using Odin Inspector to Create a Game Manager

Tools, whether plugins for game engines or separate standalone software, are useful to streamline the different processes involved in game development. Some of which even makes a character for you, rigs and animates models, and a lot more! In this tutorial, I’ll share with you how we use Sirenix‘s Odin Inspector to amp up our custom editors and create a Game Manager inside Unity.

As usual, if you want to jump ahead, you can click on a specific topic here and there are references at the end of the tutorial:

Looking for a Game Manager Solution

Our team is working on a management-strategy game called City Hall Simulator; and games in this genre involves A LOT of data that is spread across different scriptable objects from different mechanics that interact with each other. That said, if you’ve used Unity before and encountered multiple tabs, I’m pretty sure you are familiar with the crazy amount of tabs a designer needs to be opened to add and/or modify contents for your game.

To give you more context, here’s a screenshot of all the scriptable objects that we have for the AI alone for the agents we have in our pre-alpha development.

Note that this doesn’t involve data for other mechanics like Agent Needs, Zones, Objects, etc.; and we are certain that there are more content to be added to the game during our development.

Since we are a small team, I wanted to create something that will make it easier for anyone in the team to add content to the game without having to rummage through our project’s folders. With that in mind, these are the criteria for the game manager solution:

  • Can easily be expanded for new scriptable objects:
    • Adding a new editor should be as easy as adding an entry to an Enum;
    • No need to write rendering code from scratch to focus more on gameplay and systems programming. That said, we should be able to reuse our old editor windows;
  • Easy to use – have a central hub for all the game data that can easily be accessed

Now that we have the criteria, let’s move on to this tutorial’s setup.

Setup

A brief introduction on Odin Inspector – it’s (as of this writing) a third party plugin for Unity that allows developers to create custom editors without the burden of writing it from scratch. It’s as easy as adding attributes to your scriptable object’s member variables and you’re good to go. That said, here are the versions that we will be using for this tutorial:

Full disclosure: Normally, I won’t spend as much as the original price tag of Odin Inspector which is, as of writing, $55. Luckily, the plugin was included in Humble Bundle’s Fantasy Games & Game Dev Assets bundle last September 2021. Our team quickly bought the bundle in just about $25 or 1.2k Philippine Pesos, per seat.

Limitations

One last caveat before we jump into the code – limitations of using Odin Inspector. Just like any third party plugin for Unity, there are limitations with using Odin Inspector. Be sure to keep these in mind before buying the plugin so that you won’t experience hurdles in your development:

  • As of writing, Unity’s build configuration editor is not accessible. It’s part of the Platform Package from Unity for supporting build targets for DOTS. I considered this a limitation since we want all the inspector editors related to our game to be in the game manager.
  • Not redistributable, meaning everyone in the team should have a license or a copy of the plugin for them to benefit from Odin Inspector. This maybe crucial for teams using the same repo.
  • Performance – as mentioned by Sirenix themselves:

“The inspector does, of course, suffer a small performance hit in order to offer you more features; But nothing that we’ve been able to notice on our rather weak laptops.
However, when it comes to objects containing lists with thousands of elements, Odin currently suffers. We are planning on reworking how our property tree works internally in the near future, and one of the main reasons for this rework is performance.”

Code

Those out of the way, let’s start coding! If you’re not familiar with how to use Odin Inspector’s Menu Tree, I suggest starting with their videos or documentation, before continuing. I’ll still be here, don’t worry. Here are some links to the videos to help you out:

To start off our code, let me introduce the test data that we want to show in our Game Manager:

First, the AgentNeeds.cs:

[Serializable]
public class AgentNeedsMapData {
    [SerializeField]
    private string agentId;

    [SerializeField]
    private List<NeedsEditorData> needsMap;

    public AgentNeedsMapData(string goapDomainId) {
        this.agentId = goapDomainId;
        this.needsMap = new List<NeedsEditorData>();
    }

    public string GoapDomainId => this.agentId;

    [Serializable]
    public struct NeedsEditorData {
        [SerializeField]
        private NeedsEnum needId;

        [SerializeField]
        private int minHour;

        [SerializeField]
        private int maxHour;
    }

    public enum NeedsEnum {
        Bladder = 0,
        Bowel = 1,
        Food = 2
    }
}

Ideally, the NeedsEditorData struct and the NeedsEnum would be in a separate file. But, for simplicity’s sake, I included them in a single file.

The scriptable object script for the Agent Needs would look like this:

[CreateAssetMenu(fileName = "AgentNeeds", menuName = "Game/AgentNeeds", order = 0)]
public class AgentNeeds : ScriptableObject {
    [ShowInInspector]
    // [SerializeField] can also be used here
    private List<AgentNeedsMapData> agentNeedsMap = new List<AgentNeedsMapData>();

    public List<AgentNeedsMapData> AgentNeedsMap => this.agentNeedsMap;

    // =====> Getters and other resolvers needed for this mechanic is added here
}

For our second test data, we’ll be creating a simple List of colors:

[CreateAssetMenu(menuName = "Game/GameColors")]
public class GameColors: ScriptableObject {
    [ShowInInspector]
    private List<Entry>? entries;

    [Serializable]
    public struct Entry {
        public string id;
        public Color color;
    }

    // =====> Getters and other resolvers needed for this mechanic is added here
}

We could use a Dictionary here and have Odin Inspector display the Dictionary in our editor but, Dictionary is not serialized by default in Unity. Meaning, the data will be wiped out next time Unity refreshes. There is a solution for this but, that is outside of the scope of this tutorial. Let’s keep it simple for now.

Now that we have the data, let’s introduce the interface that all of our scriptable object drawers will have to implement for them to be added to the enum inside our game manager.

/// <summary>
/// This is the interface for all custom editors that will be added in the <see cref="GameManager"/>.
/// </summary>
public interface IGameManagerDrawer {
    /// <summary>
    /// This is called when building the menu tree via <see cref="GameManager.BuildMenuTree"/>
    /// </summary>
    /// <param name="tree"></param>
    public void PopulateTree(OdinMenuTree tree);

    /// <summary>
    /// This is called before the default <see cref="GameManager.DrawMenu"/>
    /// </summary>
    public void BeforeDrawingMenuTree();

    /// <summary>
    /// This is called in <see cref="GameManager.Initialize"/> when the game manager is first initialized/created.
    /// </summary>
    public void Initialize();

    /// <summary>
    /// This determines whether the game manager will use a custom editor window to render the target scriptable object
    /// or use Unity's or Odin's default window.
    /// </summary>
    public bool DisplayDefaultEditor { get; }

    /// <summary>
    /// This is the target scriptable object of this drawer.
    /// </summary>
    public object? Target { get; }
}

Now, let’s make a concrete implementation of this interface that we can inherit for custom game manager editors or just use as is for simple scriptable objects:

/// <summary>
/// A common concrete implementation of the <see cref="IGameManagerDrawer"/> interface.
/// </summary>
/// <typeparam name="T"></typeparam>
public class DrawScriptableObject<T> : IGameManagerDrawer where T : ScriptableObject {
    /// <summary>
    /// The current target scriptable object of this drawer.
    /// </summary>
    protected T? target;

    /// <summary>
    /// Cached array for where to search for the existence of a new scriptable object, when creating a new object.
    /// </summary>
    protected readonly string[] searchInFolders = new string[1];

    private const string DEFAULT_ASSETS_PATH = "Assets/";
    protected string path = DEFAULT_ASSETS_PATH;

    /// <summary>
    /// This is the path where new instances of <see cref="T"/> will be saved
    /// </summary>
    protected string Path {
        get {
            return this.path;
        }
        set {
            this.path = value;
        }
    }

    /// <summary>
    /// The current target scriptable object of this drawer.
    /// </summary>
    public virtual object? Target {
        get {
            return this.target;
        }
    }

    /// <summary>
    /// Display the default editor by default which will use whatever Unity or Odin uses in the inspector.
    /// Defaults to true so that clients can just add a new scriptable object to the game manager without worrying
    /// if a drawer/renderer is available.
    /// </summary>
    public virtual bool DisplayDefaultEditor {
        get {
            return true;
        }
    }

    public virtual void PopulateTree(OdinMenuTree tree) {
    }

    public virtual void BeforeDrawingMenuTree() {
    }

    public virtual void Initialize() {
        // Get the scriptable object from the default location
        string typeAsString = typeof(T).ToString();

        this.searchInFolders[0] = "Assets/Game/ScriptableObjects/";
        string[] foundAssetGuids = AssetDatabase.FindAssets($"t:{typeAsString}", this.searchInFolders);

        if (foundAssetGuids == null || foundAssetGuids.Length <= 0) {
            EditorUtility.DisplayDialog($"{typeAsString} Not Found!", $"There is no {typeAsString} defined anywhere under Assets/Game/ScriptableObjects/. Did you forget to create one?", "OK");
            return;
        }

        // Set the first found scriptable object of this type as the target for this drawer
        string firstMatchPath = AssetDatabase.GUIDToAssetPath(foundAssetGuids[0]);
        this.target = AssetDatabase.LoadAssetAtPath<T>(firstMatchPath);

        if (this.target == null) {
            EditorUtility.DisplayDialog($"{typeAsString} Not Found!", $"There is no {typeAsString} defined. Did you forget to create one?", "OK");
            return;
        }

        if (string.IsNullOrEmpty(firstMatchPath)) {
            EditorUtility.DisplayDialog($"{typeof(T)} Found!", $"There is no {typeof(T)} defined. Did you forget to create one?", "OK");
            return;
        }

        // Null check is performed above
        this.path = firstMatchPath;
    }
}

Note that this class is very bare-bones except for the Initialize() method which we set so that the drawer itself will search for the first available scriptable object of the specified type, instead of the user having to drag-and-drop the scriptable object as a target for this drawer.

Last thing to note here is the DisplayDefaultEditor boolean set to true by default. This is so that we can just add a new tab to the game manager without having to worry if we’ve created a custom editor for that specific type or not.

Now, for the game manager itself. This is where we will be utilizing Odin Inspector’s OdinMenuEditorWindow. Let’s start with how we want new editors to be added:

public class GameManager : OdinMenuEditorWindow {
    // ==========================================
    // =====> ADD NEW DRAWERS IN THIS LIST <=====
    // ==========================================
    // Scriptable object drawers to display in the game manager
    private readonly List<IGameManagerDrawer> drawers = new List<IGameManagerDrawer> {
        new DrawAgentNeeds(),
        new DrawScriptableObject<GameColors>()
    };

    // ============================================================
    // =====> ADD NEW GAME MANAGER STATES IN THIS ENUM (TAB) <=====
    // ============================================================
    private enum ManagerState {
        AgentNeeds,
        GameColors
    }
    
    [OnValueChanged("SetGameManagerDirty")] // Call the method "SetGameManagerDirty" when this enum changes
    [LabelText("Manager View")]
    [LabelWidth(100f)]
    [PropertyOrder(-1)] // Ensure that this enum is always drawn first
    [EnumToggleButtons] // Render this enum as toggle buttons
    [ShowInInspector]
    private ManagerState currentManagerState;


    /// <summary>
    /// Force the game manager to be dirty so that the window will be redrawn.
    /// </summary>
    public static void SetGameManagerDirty() {
        IS_DIRTY = true;
    }

    // =====> Rest of the editor code...
}

As mentioned in the goals section of this tutorial, I wanted to make adding an editor as easy as adding an entry to an enum. Luckily, this is achieved above using the attributes provided by Odin Inspector. The important attribute here is the [EnumToggleButtons] which will render the ManagerState enum as toggle buttons, which we will use as header tabs to transition between drawers or editor windows.

Another attribute that we add is the [OnValueChanged()] attribute, which we use so that we can inform Unity to re-render the game manager when the user switches tabs. We don’t want the game manager to show the Agent Needs when we’re already in the Game Colors data.

As for accessibility, let’s make a static method that we can access through Unity’s toolbar and a keyboard shortcut to open the game manager:

/// <summary>
/// Add a button in the toolbar to open the game manager with shift+alt+G as the shortcut
/// </summary>
[MenuItem("Game/Game Manager #&g")]
public static void OpenGameManager() {
    if (INSTANCE != null) {
        // If a window exists already, focus on it
        INSTANCE.Focus();
    } else {
        // Create a window and show it
        GetWindow<GameManager>().Show();
    }
}

protected override void OnDestroy() {
    base.OnDestroy();
    INSTANCE = null;
}

Next, we want to initialize the drawers and inform the renderer to draw the enum toggle buttons first before the scriptable object. First, we initialize:

/// <summary>
/// This is the index of the toolbar or the enum toggles. Cached here so that it's easy to pull it from the list and render it first
/// before the selected manager state.
/// </summary>
private int topToolBarIndex;

/// <summary>
/// This is a list of selected values in each of the manager states. This is used so that the user does not
/// need to select the same item again when returning to a state.
/// </summary>
private List<object?>? drawerTargets;

protected override void Initialize() {
    if (INSTANCE == null) {
        INSTANCE = this;
    }

    this.drawerTargets = new List<object?>();

    for (int i = 0; i < this.drawers.Count; ++i) {
        this.drawers[i].Initialize();

        // Start the target for this drawer as a null at first.
        // These values will be set when the user selects a value from the menu tree (left side)
        this.drawerTargets.Add(null);
    }

    // Add the enum tabs as the last element so that the other targets will follow their enum indexes
    // when being rendered.
    this.drawerTargets.Add(base.GetTarget());

    // Then, we just get the index of the enum tabs,
    // so that we can select it and render it at the top of the manager
    this.topToolBarIndex = this.drawerTargets.Count - 1;
}

protected override IEnumerable<object?> GetTargets() {
    // Return the cached targets if the list exists
    return this.drawerTargets ?? base.GetTargets();
}

In the Initialize() method, we basically just populate the drawers indicated in the list above and set the targets to null. These targets are the data that will be rendered by the drawers. Note that we can’t cache the base.GetTarget() and render it since OdinEditorWindow.DrawEditor() takes the target’s index as its parameter. So, we cache the index of the enum toggle buttons instead. That said, in our OnGui() method:

protected override void OnGUI() {
    // Layout event is when the content and sizes of the window is subject to change and is being computed by UnityGUI
    if (IS_DIRTY && Event.current.type == EventType.Layout) {
        ForceMenuTreeRebuild();

        if (this.currentSelectedValue == null) {
            // The selected value for the current game manager state is null, we clear the selection 
            // so that there will be nothing to render in the right side.
            // i.e. Prevent the manager from rendering the wrong value for the current selected game manager state (or scriptable object)
            this.MenuTree.Selection.Clear();
        } else {
            // The user selected a value before in the current game manager state (or scriptable object),
            // we try to select the same value again and render it.
            TrySelectMenuItemWithObject(this.currentSelectedValue);
        }

        // Reset the flag so that we don't rebuild again in the next frame
        IS_DIRTY = false;
    }

    EditorGUILayout.Space();
    // Draw the top tool bar, the enum toggle buttons
    DrawEditor(this.topToolBarIndex);
    EditorGUILayout.Space();

    // Then, render/draw the rest of the manager. See BuildMenuTree(), DrawEditors(), and DrawMenu() methods.
    base.OnGUI();
}

We call DrawEditor(this.topToolBarIndex) first to draw the toggle buttons first before allowing OnGUI() to render the rest of the editor window.

Since our game manager will have a sidebar where we can select the data to render and edit, let’s first introduce those methods:

/// <summary>
/// This is the current drawer that will be used to render/draw the <see cref="currentSelectedValue"/>
/// </summary>
private IGameManagerDrawer? currentDrawer;

protected override void DrawMenu() {
    if (this.currentDrawer == null) {
        return;
    }

    if (this.currentDrawer.DisplayDefaultEditor) {
        // Don't display the left menu if the current drawer will display the default inspector
        return;
    }

    EditorGUILayout.Space();
    // Draw the menu items based on how the drawer wanted them to look like
    this.currentDrawer.BeforeDrawingMenuTree();
    EditorGUILayout.Space();

    // Draw the menu items based on the list made in BuildMenuTree()
    base.DrawMenu();
}

The menu referred to here is the left sidebar. Note that, we don’t draw the side bar if the current drawer wants to display the default Unity or default Odin editor. The BeforeDrawingMenuTree() method of our interface allows the drawers to include other buttons, labels, or images relevant to the current target scriptable object, if they want to.

Next is how the buttons in the left sidebar is populated. For that, we’ll need to override the BuildMenuTree() method and use Odin Inspector’s OdinMenuTree:

/// <summary>
/// This is used to store the selected value before moving to a new tab. So, that the user
/// does not need to select the same item again when returning to the previous tab.
/// </summary>
private ManagerState previousManagerState;

/// <summary>
/// This is the current selected value in the <see cref="currentManagerState"/>.
/// This is the value inside the target ScriptableObject that will be rendered by the <see cref="currentDrawer"/>.
/// </summary>
private object? currentSelectedValue;

/// <summary>
/// Cache the menuTree so that we don't build a new one every time the manager is drawn.
/// We just update the contents of this instance.
/// </summary>
private OdinMenuTree? menuTree;

/// <summary>
/// This builds the tree menu at the left side of the editor window
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
protected override OdinMenuTree BuildMenuTree() {
    if (this.menuTree == null) {
        // Create a menu tree and cache it so that we don't create a new one every frame
        this.menuTree = new OdinMenuTree();

        // Draw the search bar for the menu tree in the left side of the manager.
        // This will search through whatever the drawer will add in its PopulateTree() method.
        this.menuTree.Config.DrawSearchToolbar = true;
    } else {
        this.menuTree.MenuItems.Clear();
    }

    if (this.drawerTargets == null) {
        return this.menuTree;
    }

    // Save the target of the previous drawer, so that the user won't need to select it again 
    this.drawerTargets[(int)this.previousManagerState] = this.currentSelectedValue;

    // Get the stored selected value for the current state
    int currentStateIndex = (int)this.currentManagerState;
    this.currentSelectedValue = this.drawerTargets[currentStateIndex];

    // Update the previous state
    this.previousManagerState = this.currentManagerState;

    // Populate the menu items (left side( based on the drawer
    this.currentDrawer = this.drawers[currentStateIndex];

    if (!this.currentDrawer.DisplayDefaultEditor) {
        // Populate the menu tree only if we're going to render it
        this.currentDrawer.PopulateTree(this.menuTree);
    }
    
    return this.menuTree;
}

Here, we cache the menuTree to prevent garbage every time the game manager refreshes. This is the root of the tree where the drawers will add different data as OdinMenuItems.

With the above code, we now have a game manager window and a left sidebar for the buttons representing the data in our scriptable object. Lastly, for our game manager, let’s introduce the method that will render the actual editor for the data which will occupy the rest of the right side of our game manager window:

protected override void DrawEditors() {
    if (this.drawerTargets == null) {
        // The targets list has not been initialized yet
        return;
    }

    if (this.previousManagerState != this.currentManagerState) {
        // The manager haven't drawn the updated menu tree yet
        return;
    }

    int currentStateDrawerIndex = (int)this.currentManagerState;
    
    if (this.currentDrawer != null && this.currentDrawer.DisplayDefaultEditor) {
        // Display the target by default based on how Unity and/or Odin normally display that type in the inspector
        this.drawerTargets[currentStateDrawerIndex] = this.currentDrawer.Target;
    } else {
        // Get the target for the current drawer from the menu tree (left side)
        OdinMenuTreeSelection? treeSelection = this.MenuTree?.Selection ?? null;
        this.currentSelectedValue = treeSelection?.SelectedValue;

        if (this.currentSelectedValue == null) {
            // Don't draw the editor if no menu item is selected. Keep the right side empty.
            return;
        }

        // Set the current selected value as the target to be displayed in the Manager's main body panel (right side) 
        this.drawerTargets[currentStateDrawerIndex] = this.currentSelectedValue;
    }

    // Draw the editor based on the data type of the current target, based on the current manager state
    DrawEditor(currentStateDrawerIndex);
}

The gist of this method is that we use the current tab’s index by converting the current enum, and use that index to get the drawer from our list of IGameManagerDrawers. Once we have the drawer, we set the current selected value from the left sidebar buttons as the target to be rendered by that drawer.

We should already be able to render a simple scriptable object like the GameColors data above:

But, for more complex scriptable objects, we will have to write our own custom drawer which still utilizes the IGameManagerDrawer interface. Let’s make one for our Agent Needs:

public class DrawAgentNeeds : DrawScriptableObject<AgentNeeds> {
    private string nameForNew = string.Empty;

    public override bool DisplayDefaultEditor {
        get {
            return false;
        }
    }

    public override void BeforeDrawingMenuTree() {
        if (this.target == null) {
            return;
        }

        GUILayout.BeginHorizontal();
        GUILayout.Label("New: ", GUILayout.Width(40));
        this.nameForNew = EditorGUILayout.TextField(this.nameForNew).Trim();

        if (GUILayout.Button("Add", GUILayout.Width(40), GUILayout.Height(15))) {
            if (!string.IsNullOrEmpty(this.nameForNew)) {
                // Add the new item
                CreateNew();
                this.nameForNew = "";
            }
        }
        
        // ...more buttons relevant to agent needs
    }
    
    // ...rest of the code
}

Notice that we now set the DisplayDefaultEditor boolean to false, since we want Odin to render this custom editor instead of the default one.

We also now override the BeforeDrawingMenuTree method to allow us to add functional buttons or labels in the left sidebar of the window. In our case, this is usually creation, deletion, or save buttons, and some warning or clarification labels.

Lastly, we want to populate the left sidebar’s menu tree with the agent needs so we can easily switch from one agent data to the other. To do that, we override our interface’s PopulateTree() method:

public override void PopulateTree(OdinMenuTree tree) {
    base.PopulateTree(tree);

    if (this.target == null) {
        return;
    }

    List<AgentNeedsMapData> agentNeedsMap = this.target.AgentNeedsMap;

    // Add a menu button for each goal selectors
    for (int i = 0; i < agentNeedsMap.Count; ++i) {
        AgentNeedsMapData needsMapData = agentNeedsMap[i];
        string goapDomainId = needsMapData.GoapDomainId;

        OdinMenuItem newMenuItem = new OdinMenuItem(tree, goapDomainId, needsMapData);
        newMenuItem.OnDrawItem = delegate(OdinMenuItem item) {
            GUI.backgroundColor = Color.red;

            Rect rect = new Rect(item.LabelRect);
            // This is width plus padding
            rect.x = rect.xMax - 25;
            rect.width = 20;

            // This adds a red "X" button at the right side of the menu item
            if (GUI.Button(rect, "X")) {
                if (EditorUtility.DisplayDialog($"Delete {goapDomainId}",
                    $"Are you sure you want to delete {goapDomainId}? "
                    + "This can't be undone.", "Delete", "Cancel")) {
                    this.target.AgentNeedsMap.Remove(needsMapData);
                    EditorUtility.SetDirty(this.target);
                    AssetDatabase.SaveAssets();
                    GameManager.SetGameManagerDirty();
                }
            }

            GUI.backgroundColor = Color.white;
        };

        tree.MenuItems.Insert(i, newMenuItem);
    }
}

Here we create new instances of OdinMenuItem per AgentNeedsMapData in our scriptable object. As indicated in the creation code, if you want to render more functionality in each menu item, you can set the OnDrawItem action and use GUI or GUILayout, just like you would when making custom editors in Unity. For our case, I added an “x” button to delete an entry in our AgentNeeds list.

With the DrawAgentNeeds code finished, we can add this in the drawerList and enum inside our GameManager script and have this:

This tutorial is long already but, I want to share one last piece of code. Since, I mentioned in our goal above that we want to reuse already existing editor windows that we used before, inside our game manager. To do that, instead of inheriting from Unity’s Editor class, I just adjusted our custom editors to inherit from this class and directly add them in the GameManager list and enum:

public abstract class GameManagerEditor : OdinEditor, IGameManagerDrawer {
    public virtual void PopulateTree(OdinMenuTree tree) {
    }

    public virtual void BeforeDrawingMenuTree() {
    }

    public virtual void Initialize() {
    }

    public virtual bool DisplayDefaultEditor {
        get {
            // Display the default editor by default which will use whatever Unity or Odin uses in the inspector
            return true;
        }
    }

    public virtual object? Target {
        get {
            return null;
        }
    }
}

The DisplayDefaultEditor boolean is set to true inform Odin that we already have an editor for this data type and just use that.

If you want to check the whole project, you can do so here.

One Final Note

Odin also has attributes that you can use to query all scriptable objects you want to include in the game manager automatically, as shown in this video. But, I did not use that since our project uses submodules for common utility extensions and backend systems. One of these submodules is GOAP AI which we also need in our game manager. To lessen the pain of fixing assembly references, we stick to manually adding the scriptable objects to the enum inside the GameManager.cs class. This way, we don’t need to add a dependency to Odin Inspector inside our submodules which will be used in other projects which may not support the plugin.

And there you go, this game manager has helped me in adding objects and zones to the game with ease and hopefully, it’ll help you too in making powerful tools to boost up your game development process.

If you like our posts, please subscribe to our mailing list and be among the first to know what we’re up to. You’ll also get a free game upon subscription.

If you want to see more of City Hall Simulator, please visit our YouTube channel for dev vlogs. You can also join our Discord server and directly chat with us if you have questions about this tutorial, mechanics implementations, and more. See you there!

Stay safe out there and see you in the next one!

References

A Simple Generic Timer System for Unity ECS

Timers are used in different ways in video games such as to allow the players to fast forward in strategy games, or to be used by systems for computations like damage per minute type of effect, and much more. In my experiment project using Unity’s pure ECS, I needed a timer for setting how long an NPC will be on idle.

In this quick tutorial I’ll show you how I implemented this timer system. As usual, there are references at the end of the blog for further reading. Enjoy!

The Timer Component Tag

Let’s start with the Timer tag that we will add to the entities that we want the timer system to affect. Keep in mind that we will be implementing a generic system later that will take this timer tag as the system’s data type. The tag will look like this:

public struct Timer<T> : IComponentData where T : struct {
    // Note that we don't reset the values here since
    // we will remove this component when we're done anyways

    public float ElapsedTime;
    public float TargetDuration;

    public Timer(float targetDuration) : this() {
        SetDuration(targetDuration);
    }

    public bool IsDone {
        get {
            return this.ElapsedTime >= this.TargetDuration;
        }
    }

    private void SetDuration(float targetDuration) {
        this.ElapsedTime = 0;
        this.TargetDuration = targetDuration;
    }
}

I admit that this code looks weird because we introduce a type T, but never use it anywhere in the struct. But, this will help us differentiate the different timer systems later. This is done this way as a workaround to the fact that we can’t inherit structs and structs can’t derive from classes.

The Generic Timer System

For the system, we’ll implement a generic timer system. This will allow us to extend the timer system and have different timers for different parts of the game (such as animations, idle, game speed, etc.). This is what it looks like in code:

public class TimerBaseSystem<T> : SystemBase where T : struct, IComponentData {
    private EntityQuery timerQuery;
    private EntityCommandBufferSystem entityCommandBufferSystem;

    private const float TIME_SCALE = 1f;

    /// <summary>
    /// We set the TimeScale as virtual so that subclasses can have their own timescales if needed.
    /// A use case for this is for faster speeds or faster enemies in strategy games.
    /// </summary>
    protected virtual float TimeScale {
        get {
            return TIME_SCALE;
        }
    }

    /// <summary>
    /// This is the scaled time - applying the Timescale to the delta time.
    /// </summary>
    private float ScaledTime {
        get {
            return this.Time.DeltaTime * this.TimeScale;
        }
    }

    protected override void OnCreate() {
        base.OnCreate();

        this.entityCommandBufferSystem = this.World.GetOrCreateSystem<EntityCommandBufferSystem>();

        // The main idea here is that everything that has the Timer tag of type T will only be processed
        this.timerQuery = this.EntityManager.CreateEntityQuery(
            ComponentType.ReadWrite<Timer<T>>()
        );
    }

    protected override void OnUpdate() {
        UpdateTimerJob job = new UpdateTimerJob {
            EntityTypeHandle = GetEntityTypeHandle(),
            TimerTypeHandle = GetComponentTypeHandle<Timer<T>>(),
            CurrentTimeInterval = this.ScaledTime,
            CommandBuffer = this.entityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter()
        };

        this.Dependency = job.ScheduleParallel(this.timerQuery, this.Dependency);
        this.entityCommandBufferSystem.AddJobHandleForProducer(this.Dependency);
    }

    // BURST IT!!! YAAS!
    [BurstCompile]
    private struct UpdateTimerJob : IJobChunk {
        [ReadOnly]
        public EntityTypeHandle EntityTypeHandle;

        [ReadOnly]
        public float CurrentTimeInterval;

        public ComponentTypeHandle<Timer<T>> TimerTypeHandle;

        public EntityCommandBuffer.ParallelWriter CommandBuffer;

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            NativeArray<Entity> entities = chunk.GetNativeArray(this.EntityTypeHandle);
            NativeArray<Timer<T>> timers = chunk.GetNativeArray(this.TimerTypeHandle);

            for (int i = 0; i < entities.Length; ++i) {
                Timer<T> timer = timers[i];

                timer.ElapsedTime += this.CurrentTimeInterval;

                if (timer.IsDone) {
                    int sortKey = firstEntityIndex + i;
                    this.CommandBuffer.RemoveComponent<Timer<T>>(sortKey, entities[i]);
                }

                timers[i] = timer;
            }
        }
    }
}

Now, if you want to extend the timer system – say, you want another one with a faster timescale, it will look like this:

public struct FastTimeScaleTag : IComponentData {
}

public class FastTimeScaleTimerSystem : TimerBaseSystem<FastTimeScaleTag> {
    protected override float TimeScale {
        get {
            return 2f;
        }
    }
}

We first create a new tag that we will then use as the type for the new timer system. Then inside the timer system, we override the TimeScale and set it higher (faster) than the default 1 inside the base class.

A caveat however, if you run this now, you will get an ArgumentException error. This is because in ECS, all ComponentTypes should be known during compile time. To remedy this you can create a separate file, say AssemblyInfo.cs and add the following:

[assembly: RegisterGenericComponentType(typeof(Timer<NormalTimeScaleTag>))]
[assembly: RegisterGenericComponentType(typeof(Timer<FastTimeScaleTag>))]

You can also add this in the TimerBaseSystem class right after declaring your using directives if that is more convenient for you.

To further make it easier to track all the timer systems, we can make a separate system group for all the timers. Then update our timer systems inside that group.

// Depending on your use case, you can choose when your timers should update
[UpdateBefore(typeof(EndSimulationEntityCommandBufferSystem))]
public class TimerSystemsGroup : ComponentSystemGroup {
}

[UpdateInGroup(typeof(TimerSystemsGroup))]
public class TimerBaseSystem<T> : SystemBase where T : struct, IComponentData {
// Rest of the timer system code
}

Now, we are almost set. We just need to add the timer tags to our entities. My use case for this timer is for how long an NPC should wait (be idle) before moving. That said adding the components for me would look something like:

// This will make the NPC wait for 3 seconds at a normal time scale.
// More or less 3 seconds in real life.
this.CommandBuffer.AddComponent(firstEntityIndex, npcEntity,
    new Timer<NormalTimeScaleTag>(3f)
);

// This will make the NPC move earlier than the one above with a
// faster timescale which is twice of the normal time scale
// which is about 1.5 seconds in real life.
this.CommandBuffer.AddComponent(firstEntityIndex, npcEntity,
    new Timer<FastTimeScaleTag>(3f)
);

To demonstrate this I made the following setup:

  • Two NPCs with different time scales (1f and 2f), but the same idle wait time of 3 seconds; and,
  • A MovementSystem that would move the two NPCs after waiting

And it looks something like this (note that timing might differ since I just captured this with a screen to gif tool):

Unity Pure ECS Generic Timer System Example
Don’t worry, they still met in the end and lived happily ever after. *wink*

And there you have it. For further improvement, I’m thinking of adding a query in the base system that the subclasses can override, so that other parts of the game can get the scaled time from any respective timer systems. I already got it working and if you want to read more about query systems, Marnel from Squeaky Wheel wrote a simple query system. He made a better version of that system that doesn’t involve boxing but I can’t seem to find a reference about it online, maybe soon.

If you have questions you can reach me via Twitter or Instagram – That’s all for now and see you in the next one!

References:

P.S. It took a lot longer for me to release this blog because of something great happening soon – Academia: School Simulator is releasing in just a couple of weeks and boy are we nervous in Squeaky Wheel. Please check it out on Steam and let us know what you think. Thank you again, have a good one, and stay safe!

Working with Scriptable Objects and Blob Assets, and creating a Utility Class

So, you want to convert scriptable objects to data that you can use in your ECS systems – but, you can’t directly access SOs using pure ECS. Today, I’m going to share with you how to convert scriptable objects to Blob Assets and also share a utility class that I created while studying Blob Assets.

First off, let’s quickly define what is a blob asset – it’s an immutable data that can be accessed by your systems across multiple threads. Meaning, you can use these in running parallel jobs, which is really important for creating performant systems using Unity’s DOTS. It’s also important to note that you can only use blittable types in your blob assets, and BlobString or FixedString if you want to use strings.

If you want to learn more about getting started with blob assets, Marnel from Squeaky Wheel wrote a guide to help you out. And Code Monkey created an easy-to-understand video, “What are Blob Assets?”. Also, check Unity’s test scripts regarding Blob Assets, they help a lot in understanding how things work.

Before we start – for context, I’m making an NPC generator as a test project for studying ECS. The code here is stable with the packages – Entities 0.16.0-preview.21 and Hybrid Renderer 0.8.0-preview.19.

Jumping in, the best use case for Blob Assets is converting Scriptable Objects (designer-friendly and can be edited in Unity’s editor) to data that can be used in your jobs with burst.

[EDIT – Sept. 12, 2021]

If you’re encountering this error (I encountered this with the packages – Entities 0.17.0-preview.42 and Hybrid Renderer 0.11.0-preview.44 – other dependent packages should update accordingly):

error ConstructBlobWithRefTypeViolation: You may not build a type TBlobAssetType with Construct as TBlobAssetType is a reference or pointer.  Only non-reference types are allowed in Blobs.

Unity is already aware of this issue (see this thread for more info), but the fix is included in Entities 0.18, of which there is no ETA yet as of the time of writing of this edit block. In the meantime you can implement this workaround from this post:

"For now, you can comment out loop on line 149 in BlobAssetSafetyVerifier.cs"

I’ll see if I can find another workaround, until then I’ve implemented the workaround above and the project in this tutorial should still work.

[END OF EDIT BLOCK]

Converting Scriptable Objects

Let’s first create the Scriptable Object that we want to convert. Keeping it simple, let’s add an integer for the maximum number of NPCs we want to generate and, for fun, a maximum number of friends an NPC can have.

[CreateAssetMenu(fileName = "NpcManagerData", menuName = "Game/NpcManagerData")]
public class NpcManagerData : ScriptableObject {
    [SerializeField]
    private int totalNumberOfNpcs;

    [SerializeField]
    private int totalFriends;
  
    public int TotalNumberOfNpcs => this.totalNumberOfNpcs;

    public int TotalFriends => this.totalFriends;
}

We also need a gameobject that will hold this scriptable object,

public class DataContainer : MonoBehaviour {
    [SerializeField]
    private NpcManagerData npcManagerData;

    // Accessor for the conversion system
    public NpcManagerData NpcManagerData => this.npcManagerData;
}

In order to convert this to a blob asset, we need to define the structure of the blob asset. For our purposes, the structure will be pretty similar. You can have computations or a specific conversion logic in the conversion system later, if the need arises.

public struct NpcDataBlobAsset {
    public int TotalNumberOfNpcs;
    public int TotalFriends ;
}

At this point, it’s also important to note that Scriptable Objects are “scene data” – meaning they exist in the “game object” world of Unity. That said, we need a way to convert these “scene data” to DOTS. We can achieve this by using a GameObjectConversionSystem,

[UpdateInGroup(typeof(GameObjectConversionGroup))]
public class TestGameDataSystem : GameObjectConversionSystem {
    // We made this static so that other systems can access the blob asset.
    // We'll modify this later to work with job systems. 
    // For now, let's keep it simple.
    public static BlobAssetReference<NpcDataBlobAsset> NpcBlobAssetReference;
    
    protected override void OnCreate() {
        base.OnCreate();

        // Let's debug here to make sure the system ran
        Debug.Log("Prefab entities system created!");
    }

    protected override void OnUpdate() {
        // Access the DataContainer attached to a gameObject here and copy the data to a blob asset
        this.Entities.ForEach((DataContainer container) => {

            // We use a using block since the BlobBuilder needs to be disposed after using it
            using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp)) {

                // Take note of the "ref" keywords. Unity will throw an error without them, since we're working with structs.
                ref NpcDataBlobAsset npcDataBlobAsset = ref blobBuilder.ConstructRoot<NpcDataBlobAsset>();

                // Copy data. We'll work with lists/arrays later.
                npcDataBlobAsset.TotalNumberOfNpcs = container.NpcManagerData.TotalNumberOfNpcs;
                npcDataBlobAsset.TotalFriends = container.NpcManagerData.TotalFriends;
                
                // Store the created reference to the memory location of the blob asset
                NpcBlobAssetReference = blobBuilder.CreateBlobAssetReference<NpcDataBlobAsset>(Allocator.Persistent);
            }
        });

        // Print to check if the conversion was successful.
        // Note that we have to access the "Value" of where the reference is pointing to.
        Debug.Log($"At prefab entities initialization: total npc count is {NpcBlobAssetReference.Value.TotalNumberOfNpcs.ToString()}");
    }
}

If you are not familiar with the code above, check Unity’s talk during Unite Copenhagen 2019 on “Converting scene data to DOTS”. It’s important to note that GameObjectConversionSystems work in the world in between the GameObject/Scene world (where gameobjects exist like Prefabs, Scriptable Objects, etc.) and the Entity or “DOTS world” (where your systems are). I’m probably oversimplifying here since you can have multiple worlds. In that case, the GameObjectConversionSystems still lie between the GameObject/Scene world and your worlds.

Before we test the code, make sure that you have the DataContainer (the one where you put the references to the scriptable object) in a subscene. Because subscenes convert the gameObjects in them to Entites when you “close” them in the Inspector. See Unity’s Unite Copenhagen talk for more info. Here’s what the hierarchy looks like in the editor,

Running the game will print these in the console,

Yey, it works!

Cool, now we converted the Scriptable Object to a Blob Asset that can be accessed using the static BlobAssetReference in our TestGameDataSystem. Let’s add two more things – an entity with the blob asset reference that can be accessed in our job systems, and the promised blob asset utility class.

Accessing Blob Assets in DOTS/ECS Systems

First, we need a component data that we will attach to the entity,

// This will be used by job systems to access blob asset data,
// since we cannot access static non-readonly fields in jobs
public struct BlobAssetReferences : IComponentData {
    public BlobAssetReference<NpcDataBlobAsset> NpcManager;
}

Next, let’s add this component to a new entity which we’ll create right after generating the NpcDataBlobAsset, in our GameObjectConversionSystem earlier,

        // ...the rest of the GameObjectConversionSystem earlier

        Debug.Log($"At prefab entities initialization: total npc count is {NpcBlobAssetReference.Value.TotalNumberOfNpcs.ToString()}");

        // We use the default world here since this is attached to a gameobject in a subscene which is in itself, a World.
        // We have 3 worlds at this point: Default, Subscene, and Subscene entity conversion world
        EntityManager defaultEntityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        
        Entity gameDataEntity = defaultEntityManager.CreateEntity();
        BlobAssetReferences blobAssetReferences = new BlobAssetReferences {
            NpcManager = NpcBlobAssetReference
        };
        defaultEntityManager.AddComponentData(gameDataEntity, blobAssetReferences);
    }
}

Be careful when creating entities in GameObjectConversionSystems since there are 2 EntityManagers there, since we’re working with 2 worlds while in the GameObjectConversionSystems – one that is used in the conversion world, and one that is used in the default world. If you are creating an entity that you want to use in the default world, use the World.DefaultGameObjectInjectionWorld.EntityManager and not the EntityManager in the GameObjectConversionSystems.

Now, let’s create a simple system to see if we can access the blob asset from a job system and run the game,

[UpdateInGroup(typeof(SimulationSystemGroup))]
public class TestSystem : SystemBase {
    protected override void OnUpdate() {
        if (!TestGameDataSystem.NpcManager.IsCreated) {
            // Don't do anything if the NpcManager is not yet created
            return;
        }

        this.Entities.ForEach((Entity entity, int entityInQueryIndex, ref BlobAssetReferences blobAssetReferences) => {
            NpcDataBlobAsset npcDataBlobAsset = blobAssetReferences.NpcManager.Value;

            for (int i = 0; i < npcDataBlobAsset.TotalNumberOfNpcs; i++) {
                // you can now access the NpcDataBlobAsset here
            }
        }).ScheduleParallel();
    }
}
You can see here that we can now use the BlobAssetReferences component in our systems

Blob Asset Utility Class

Cool! Next, let’s create another blob asset, but this time let’s refactor the BlobBuilder in our TestGameDataSystem to a utility class, so that we can simplify our code and make it easier to read,

public static class BlobAssetUtils {
    private static BlobBuilder BLOB_BUILDER;

    // We expose this to the clients to allow them to create BlobArray using BlobBuilderArray
    public static BlobBuilder BlobBuilder => BLOB_BUILDER;

    // We allow the client to pass an action containing their blob creation logic
    public delegate void ActionRef<TBlobAssetType, in TDataType>(ref TBlobAssetType blobAsset, TDataType data);

    public static BlobAssetReference<TBlobAssetType> BuildBlobAsset<TBlobAssetType, TDataType>
        (TDataType data, ActionRef<TBlobAssetType, TDataType> action) where TBlobAssetType : struct {
        BLOB_BUILDER = new BlobBuilder(Allocator.Temp);
        
        // Take note of the "ref" keywords. Unity will throw an error without them, since we're working with structs.
        ref TBlobAssetType blobAsset = ref BLOB_BUILDER.ConstructRoot<TBlobAssetType>();

        // Invoke the client's blob asset creation logic
        action.Invoke(ref blobAsset, data);

        // Store the created reference to the memory location of the blob asset, before disposing the builder
        BlobAssetReference<TBlobAssetType> blobAssetReference = BLOB_BUILDER.CreateBlobAssetReference<TBlobAssetType>(Allocator.Persistent);

        // We're not in a Using block, so we manually dispose the builder
        BLOB_BUILDER.Dispose();

        // Return the created reference
        return blobAssetReference;
    }
}

As for our TestGameDataSystem, the OnUpdate function will look like,

protected override void OnUpdate() {
    this.Entities.ForEach((DataContainer container) => {
        // Use the Utility class here - pass the container data, then the conversion logic as an action
        NpcBlobAssetReference = BlobAssetUtils.BuildBlobAsset(container.NpcManagerData, delegate(ref NpcDataBlobAsset blobAsset, NpcManagerData data) {
            blobAsset.TotalNumberOfNpcs = data.TotalNumberOfNpcs;
            blobAsset.TotalFriends = data.TotalFriends;
        });
    });

    // ...the rest of the code
}

Looking clean already. Now, to give you more idea on the use cases of Blob Assets, let me share a simple library of names (using lists) I created and converted to a blob asset for my NPCs. Starting with the Scriptable Object,

[CreateAssetMenu(fileName = "NamesData", menuName = "Game/NamesData")]
public class NamesData : ScriptableObject {
    public List<string> firstNames;
    public List<string> lastNames;
}

Now the blob asset,

public struct NamesBlobAsset {
    // Blob arrays are memory location offsets from the original Blob Asset Reference.
    // At least, that's how I understood the manual.
    public BlobArray<FixedString32> FirstNames;
    public BlobArray<FixedString32> LastNames;
}

Lastly, the creation logic in the TestGameDataSystem,

protected override void OnUpdate() {
    this.Entities.ForEach((DataContainer container) => {

        // ...the NpcBlobAssetReference generation code here

        NamesLibraryReference = BlobAssetUtils.BuildBlobAsset(container.NamesData, delegate(ref NamesBlobAsset blobAsset, NamesData data) {
            // Cache the blob builder from the utility class so we can generate blob arrays
            BlobBuilder blobBuilder = BlobAssetUtils.BlobBuilder;

            BlobBuilderArray<FixedString32> firstNamesArrayBuilder = blobBuilder.Allocate(ref blobAsset.FirstNames, data.firstNames.Count);
            BlobBuilderArray<FixedString32> lastNamesArrayBuilder = blobBuilder.Allocate(ref blobAsset.LastNames, data.lastNames.Count);

            for (int i = 0; i < data.firstNames.Count; ++i) {
                // Copy the data from the list to the BlobBuilderArray
                firstNamesArrayBuilder[i] = new FixedString32(data.firstNames[i]);
            }

            for (int i = 0; i < data.lastNames.Count; ++i) {
                lastNamesArrayBuilder[i] = new FixedString32($" {data.lastNames[i]}");
            }
            
            // We don't have to worry about "storing" the created array to the blob asset here 
            // since that is already handled in the BlobAssetUtils. This is just the creation logic
            // that is passed to the utility class.
        });
    });

    // ...the rest of the code
}

And that’s it. Blob Assets are a great way of storing data when working with Unity’s DOTS and there are still a lot to learn. I did stumble into some hurdles and errors here and there while trying to make a simple NPC generator. I’m still working on it but I’ll make sure to write another article/guide when I make any significant progress. See you in the next one!

References:

Neil Creates: Dice Hunter

Master tactical dice battles! Enjoy the clever combination of skill and luck. Dice Hunter is a turn-based role playing game with collectible dice. Play now for FREE!
Become the Dicemancer and assume the incredible ability to capture creatures into dice. Hunt dice and wield them to save the Land of Chance from the marauding minions and evil allies of the wicked Snake Eyes. – Greener Grass, Dice Hunter: Quest of the Dicemancer Google Play Store Page

Start To Finish Video:

Part 1: Making the Dice

First off, I made the dices using Blender (which you can get on Steam now). I used the default cube that’s already on the scene upon startup. Then, I added a cylinder and duplicated it (in edit mode).

NC2_Blender_MakingCylinders

After that, I applied a Boolean modifier to the cube and set the object parameter to the cylinder that I’ve created in the previous step; Finally, I set the operation parameter to Difference. This way, Blender will ‘subtract’ the area of the cylinder that intersects with the cube and close the inside of the ‘holes’ made.

NC2_Blender_BooleanOperation
Before the operation

NC2_Blender_AfterOperation
After the operation

After that, you’ll notice that there were some ugly faces on the cube (in edit mode). This is because the boolean operation doesn’t take any consideration about triangles or quads. It’ll just perform the operation and connect the vertices of the main object and the ‘holes’ made during the operation.

NC2_Blender_Solved!

In order to fix this, I applied another modifier to the cube – Remesh. This modifier re-solves the mesh and figure out another way of distributing the vertices of that mesh. You can see it here, after applying the remesh modifier, I came up with a cube with these vertices:

NC2_Blender_RemeshedCube
Neat!

After that, in order to get rid of the sharp edges (real life dices doesn’t have those sharp edges. Well, not that I know of.), I applied another modifier – Yes, I love modifiers – to the mesh, a Subsurf modifier.

NC2_Blender_Subsurf

After that, you can add another modifier, a Decimate modifier, in order to reduce the number of vertices. Or, you can just reduce the number of subsurf steps. After making the cube, I exported it to a FBX file and imported it in Unity.

Part 2: Unity Setup

In Unity, I made a simple scene with a plane (for the ground) and six dices, just like in Dice Hunter.

NC2_Unity_Setup

Part 3: PROGRAMMING!!!

To start off, I added a RigidBody component to each of the die. This is because I want to make the dices react or have a realistic motion/physics. After that I made a variable referencing the RigidBody component and applied a force to the dices every time the left mouse button is pressed.


using UnityEngine;
public class DiceTossScript : MonoBehaviour {
private static float force = 150f;

private Rigidbody rb;

private void Start() {
 rb = GetComponent<Rigidbody>();
}

private void FixedUpdate() {
 if (Input.GetMouseButtonDown(0)) {
   rb.AddForce(Vector3.up * force);
 }
}
}

The reason why I used Vector3.up instead of transform.up is because, the latter one will change when the object rotates while the former is constantly pointing to the ‘up of the world’, the global up, whatever you call it, the up of everything.

After that I added another piece of code in the FixedUpdate function which will add a torque (or rotation force, if you will) to the cube which will make it rotate randomly.

private void FixedUpdate() {
if (Input.GetMouseButtonDown(0)) {
//This generates a random point to apply the torque
//This way, the dices will rotate randomly
var temp = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));

rb.AddForce(Vector3.up * force);
rb.AddTorque(temp * 1000f);
}
}

I also added a physics material that has a bounciness in it. Then, I set the friction to minimum in order to prevent the dices from ‘standing’ on their edges. This happens because of the friction between the edges and the ground which makes the dice fall super slowly, which does not happen in real life (well, maybe…).

Then for determining which face is up, I added triggers on each side of the die. The main logic I thought of is that, the trigger will check if it’s hitting the ground; if it does, the die will return the number of the opposite side.

NC2_Unity_

After that, I wrote a script that will be applied on each of these triggers with will perform the logic stated above.

using UnityEngine;

public class SideIndicator : MonoBehaviour {
private void OnTriggerStay(Collider other) {
if (other.transform.name == "Ground") {
var parent = transform.parent.GetComponent<DiceTossScript>();
var temp = int.Parse(transform.name);
DiceTossScript.combination[parent.diceIndex] = temp;
}
}
}

I also added a static array of integers in the DiceTossScript in order to hold the combination of the faces up. Just like in Dice Hunter, this can be used in order to determine how many ‘attack’ faces are up for combos and other power ups.

After that, I also added a diceIndex on each die which corresponds to an index in the array of integers. That is, die 1 is mapped to array[0], die 2 is mapped to array[1], and so on.

//Declaration of the int array and the diceIndex variables
public static int[] combination;
public int diceIndex = 0;

private void Start() {
rb = GetComponent<Rigidbody>();

//Initialization of the array that has six elements
//Each element corresponds to each dice
combination = new int[6];
}

Then, I noticed that there was unnecessary rotation in the y-axis when the dices land, which does not happen in Dice Hunter. This is fixed by multiplying the angularVelocity of the RigidBody to 0, which basically stops it, when the dices are already close to their initial y-position. I did this in the update function, you can also perform this checking in the LateUpdate function, if you want to.

private void Update() {
 if(transform.position.y &lt; 0.35f)
 rb.angularVelocity *= 0f;
}

And that’s it. Moving on from here, you can use the combination array in order to build more on a game mechanics.

Here are the final codes:

For the DiceTossScript.cs:

using UnityEngine;

public class DiceTossScript : MonoBehaviour {
private static float force = 150f;
public static int[] combination;

public int diceIndex = 0;
private Rigidbody rb;

private void Start() {
rb = GetComponent<Rigidbody>();
combination = new int[6];
}

private void Update() {
if(transform.position.y < 0.35f)
rb.angularVelocity *= 0f;
}

private void FixedUpdate() {
if (Input.GetMouseButtonDown(0)) {
var temp = new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));rb.AddForce(Vector3.up * force);
rb.AddTorque(temp * 1000f);
}
}
}

 

And for the SideIndicator.cs:

using UnityEngine;

public class SideIndicator : MonoBehaviour {
  private void OnTriggerStay(Collider other) {
   if (other.transform.name == "Ground") {
    var parent = transform.parent.GetComponent<dicetossscript>();
    var temp = int.Parse(transform.name);
    DiceTossScript.combination[parent.diceIndex] = temp;
   }
  }
}

There you have it, I hope you enjoyed the tutorial. There were errors here and there and that is the beauty of learning – we make mistakes and we learn from them.

If you have comments, suggestions, recommendations, and questions, comment them down below and let’s have a healthy discussion.

Thank you and see you in the next one!

Neil Creates: Don’t Starve

Don’t Starve is an uncompromising wilderness survival game full of science and magic. Enter a strange and unexplored world full of strange creatures, dangers, and surprises. Gather resources to craft items and structures that match your survival style. – Klei EntertainmentDon’t Starve Steam Page

Part 1: Set Up

First off, I made the sprite sheet for the game first in order to have something to work with, using Photoshop.

Capture.PNG

As you might notice, I only used one sheet for both the player and the environment assets. I did this only for the sake of the tutorial. If you’re going to make a game, it’s a great practice to separate the static (mostly environment and props) sprites from those with animations (characters, effects, etc.).

Capture

After finishing up with the sheet on Photoshop. I made a separate PNG file which I will then manipulate in Unity. Now, Unity can actually read .psd files right off the bat. But, I kept the .psd file of my sheet separate from what I actually used in Unity (I also do this with FBX and .blend files when doing 3D). This may add another step in the pipeline and I can’t really say any benefits from doing this, but for me, it’s another great practice to backup and keep all ‘raw’ or original files separate from the one being used in the engine.

Capture.PNG

As for slicing the sheet, you can use Unity’s built-in sprite editor or use a 3rd party application, whichever you feel more comfortable working with (I used Unity’s built-in sprite editor). Then, you can automate this process and you can also slice manually, especially if you have a specific requirement with the sprites. Also, I kept the pivot points of the sprites in the middle – this is not usually the case, especially with characters that have different sprites for different parts of their body.

For example, you have a character which is composed of separate sprites (shoulder, arms, hands, etc.) and you will be applying inverse kinematics where the hand is the target from the shoulder. In a character’s arm, you don’t want the pivot of the shoulder to be in the middle of the sprite ‘cut’; this will make it difficult to manipulate, because the rotation of the shoulder will be relative to where the pivot is, in this case, the shoulder will rotate weirdly. Instead, you want the pivot to be where the shoulder joint is; this way, the shoulder will ‘naturally’ move. It’s important to keep requirements like this in mind when slicing up sprite sheets in order to avoid going back and making changes which will be a pain, especially if you’re already in the middle of production.

Capture

I then added an Animator component to the character; this will hold the parameters (that we will be accessing via code) which will determine which animation will be played (idle, walking, and attacking). For producing the animations, I used Unity’s animation tab to cycle through my sprites.

You can say that, Unity pretty much has it all *wink*.

Capture

In order to control the animations, I set up the parameters and the transition logic of the animations in Unity’s Animator tab. I used a float as a parameter to transition between the ‘idle’ and the ‘walk’ animations, which may not be the best option since you can use a boolean to determine if the character is moving or not. But, I suggest that you get used to using floats because you can have a walking animation and a running animation, which will be determined via the speed/velocity – which is mostly a float – of your character. Then for the attack, I used a boolean because the player can be ‘attacking’ or ‘not attacking’.

Part 2: Programming

As mentioned in the video, there are multiple ways of moving objects in Unity:

  • transform.Translate()
  • RigidBody (Forces, VelocityChange, and MovePosition mostly)
  • Character Controller
  • Unity’s NavMesh (I don’t think this is a good idea, in main characters, but GREAT WITH AI)
  • Changing the object’s Transform component via code (works sometimes, but not advisable)

For our player, I added a rigidBody component and moved the sprite around in 3D space via VelocityChange.

Here are the codes:

For the player,


using UnityEngine;
public class PlayerScript : MonoBehaviour {

[SerializeField] //These are used to make the variables show up in the inspector
private float moveSpeed;
[SerializeField]
private float hitDistance;
private float currentSpeed;
private Animator anim;
private Rigidbody rb;

private int woodCount = 0;

private float maxAttackSpeed = 1.2f, attackTimer = 0;

void Start () {
   anim = GetComponent<Animator>();
   rb = GetComponent<Rigidbody>();
}

private void Update() {
 if (Input.GetMouseButton(0)) {
  interact();
  anim.SetBool("isAttacking", true);
 } else {
  anim.SetBool("isAttacking", false);
 }
}

void FixedUpdate () {
 if (Input.GetButton("Horizontal")) {
  currentSpeed = Input.GetAxis("Horizontal") * moveSpeed * Time.deltaTime;

  if(currentSpeed < 0) {
   GetComponent<SpriteRenderer>().flipX = true;
  } else {
   GetComponent<SpriteRenderer>().flipX = false;
  }

  rb.AddForce(currentSpeed * transform.right, ForceMode.VelocityChange);
 }else if (Input.GetButton("Vertical")) {
  currentSpeed = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime;
  rb.AddForce(currentSpeed * transform.forward , ForceMode.VelocityChange);
 } else {
  anim.SetFloat("Speed", 0f);
  rb.velocity *= 0.5f;
 }

 if(currentSpeed != 0) {
  anim.SetFloat("Speed", rb.velocity.magnitude);
 }
}

private void interact() {
 RaycastHit hit;

 if(attackTimer > maxAttackSpeed) {
  if(Physics.Raycast(transform.position, Mathf.Sign(currentSpeed) * Vector3.right, out hit, hitDistance)) {
   if (hit.transform.CompareTag("Tree")) {
    woodCount++;
   }
  }

  attackTimer = 0;
 } else {
  attackTimer += Time.deltaTime;
 }
}
}

 

For the Camera,


using UnityEngine;

public class CameraFollowScript : MonoBehaviour {

private Transform player;
[SerializeField]
private float panSpeed;
[SerializeField]
private float height;
[SerializeField]
private float distance;

private RaycastHit hit;
private Ray rayFromCamera;

void Start () {
player = GameObject.FindGameObjectWithTag("Player").transform;
}

void Update () {
rayFromCamera = new Ray(transform.position, transform.forward);

if(Physics.Raycast(rayFromCamera, out hit, 3f)) {
if (!hit.transform.CompareTag("Player")) {
var obstacle = hit.transform.gameObject;
if (obstacle.GetComponent<SpriteRenderer>() != null) {
obstacle.AddComponent<ChangeAlpha>();
}
}
}

var newPosition = new Vector3(player.position.x, player.position.y + height, player.position.z - distance);
transform.position = Vector3.Lerp(transform.position, newPosition, panSpeed * Time.deltaTime);
}
}

For changing the alpha of the object between the camera and the player,


using UnityEngine;

public class ChangeAlpha : MonoBehaviour {

private SpriteRenderer currentSprite;

void Start() {
currentSprite = GetComponent<SpriteRenderer>();

var currentColor = currentSprite.color;
currentColor.a = 0.8f;

currentSprite.color = currentColor;
}

void Update() {
var currentColor = currentSprite.color;
currentColor.a = 1f;

currentSprite.color = currentColor;
Destroy(this);
}
}

There is a flaw in my logic here. You might have noticed that when you play the game, it the ChangeAlpha code works. But, it is not efficient – this is a wrong implementation, if I will be honest.

Why? Because, the script is CONTINUOUSLY adding and removing the script on the object that is between the camera and the player which consumes a lot of resources. This is also the reason why you can’t see the component in the inspector.

Short explanation: the ChangeAlpha script is being added to the object in the CameraFollowScript then it is being removed in the Update function (which runs every frame) in the ChangeAlpha script.

Having triggers behind the obstacles may be a better option. Triggers that detects if the player (tagged as “Player) is behind the object, then changing the alpha of the obstacle, if so.

There you have it, I hope you enjoyed the tutorial. There were errors here and there and that is the beauty of learning – we make mistakes and we learn from them.

If you have comments, suggestions, recommendations, and questions, comment them down below and let’s have a healthy discussion.

Thank you and see you in the next one!