D20 RPG – Positional Awareness

“A constant struggle, a ceaseless battle to bring success from inhospitable surroundings, is the price of all great achievements” – Orison Swett Marden

Overview

This project has provided an easy way to grab a GameObject that serves as the “view” for an Entity, but currently does not provide a way to do the reverse. If you had a GameObject, how would you know what Entity was associated with it? Or suppose you only know a target position and want to know if anything is there?

There may be many reasons why you might want such a flow, such as when responding to a physics event – perhaps a character enters a “trigger” area, like a trap or door. You could grab the Collider and then its GameObject – now you would just need the associated Entity.

As I start fleshing out the “tactics” side of the game, where position actually matters, then I could also use physics to see what is within range of an attack or a cast spell. Once again, you could use Unity’s Physics engine to simply do an overlap check at the desired location. The resulting collider(s) could give you GameObjects with which you might want an associated Entity.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Entity View

A very simple way to get an associated Entity for any given GameObject is to create a Component to hold that data. As a side benefit, you can look at the script in the inspector and see the Entity’s id which may be useful for a variety of debugging purposes. Go ahead and create a new C# script at Scripts -> SoloAdventure -> Encounter named EntityView and add the following:

using UnityEngine;

public class EntityView : MonoBehaviour
{
    public Entity entity;
}

Entity View Provider

A simple way to make sure that our EntityView is correctly associated with an Entity is to make sure that we update that component whenever assigning a “view” to an Entity. Open the EntityViewProvider script and modify the SetView method to look like this:

public void SetView(GameObject view, Entity entity, ViewZone zone)
{
    if (!mapping.ContainsKey(zone))
        mapping[zone] = new Dictionary<Entity, GameObject>();

    if (view)
    {
        mapping[zone][entity] = view;
        var ev = view.GetComponent<EntityView>();
        if (ev == null)
            ev = view.AddComponent<EntityView>();
        ev.entity = entity;
    }
    else
        mapping[zone].Remove(entity);
}

When we set an Entity’s view to a non-null value, in addition to setting the mapping, we now attempt to get the EntityView component off of the GameObject. If we don’t find one already attached, then we add one manually. One way or another the GameObject will now have an EntityView component and we can set its “entity” field to match.

Physics System

Create a new C# script in the same folder named PhysicsSystem and add the following:

using UnityEngine;

public interface IPhysicsSystem : IDependency<IPhysicsSystem>
{
    Entity? OverlapPoint(Point point, int layerMask);
}

public class PhysicsSystem : IPhysicsSystem
{
    const int maxResultCount = 10;
    Collider2D[] results = new Collider2D[maxResultCount];

    public Entity? OverlapPoint(Point point, int layerMask)
    {
        Vector2 pos = new Vector2(point.x + 0.5f, point.y + 0.5f);
        var resultCount = Physics2D.OverlapPointNonAlloc(pos, results, layerMask);
        for (int i = 0; i < Mathf.Min(resultCount, maxResultCount); ++i)
        {
            var entityView = results[i].GetComponent<EntityView>();
            if (entityView)
                return entityView.entity;
        }
        return null;
    }
}

Here I have a new interface with a single method. It indicates that I will use Physics to test if an Entity is present at a given Point. Since it may be possible for multiple GameObjects with Entity associations to overlap, such as if a Hero were standing on a trap, I also accept a parameter for the "layerMask" to cast against. In the future we may even want additional methods to return collections of Entities, and may desire "areas" to check against as well.

In the class I have decided to implement this interface's method using Physics2D.OverlapPointNonAlloc which lets me do a physics check that does not allocate a new array of colliders each time I use it. Therefore I needed to create my own array that could be re-used. I added the "results" array field for that purpose.

Because of the grid-based nature of my game, I position my sprites at whole number coordinate positions like "(2, 3)". So far my characters are about a unit wide. The collider for my characters was intentionally scaled down just a bit, to help avoid unintended physics collisions from adjacent units. Therefore, when I do my physics check, I won't want to check their actual position, but instead will check in the "middle of their square" by adding 0.5 to both the X and Y position.

In most cases, the number of objects that Physics2D.OverlapPointNonAlloc finds will not match the capacity of the array that I provide to it. The method will not resize my array (otherwise it would need to allocate), so I need to track the number of matching results that it found. Then I loop over the possible matches (making sure not to exceed the bounds of the array) and for each match I attempt to grab an EntityView component. As soon as I find one, I return its "entity" - or in the event that no match is found, I will simply return null.

Solo Adventure Injector

Open the Solo Adventure Injector script and add the following to its Inject method:

IPhysicsSystem.Register(new PhysicsSystem());

Solo Adventure Attack

For the purposes of easier demonstration of our new feature, let's go ahead and modify the solo adventure attack such that when attacking, the player must move the cursor to the position of the monster that they wish to target. This will be especially important when we have multiple enemies, because at the moment, we would only ever be able to attack the first one.

Open the SoloAdventureAttack script and add the following method:

async UniTask<Entity> SelectTarget(Entity entity)
{
    if (entity.Party == Party.Monster)
        return ICombatantSystem.Resolve().Table.First(c => c.Party != entity.Party);

    int layerMask = LayerMask.GetMask("Hero");
    Entity? target = null;
    while (!target.HasValue)
    {
        var position = await IPositionSelectionSystem.Resolve().Select(entity.Position);
        target = IPhysicsSystem.Resolve().OverlapPoint(position, layerMask);
    }
    return target.Value;
}

At the beginning of the method, I first check whether or not the attacking entity is a monster, and if so, will provide a target the same way we had been doing, by simply picking the first hero available.

When it is the player's turn though, we want to provide handling so that the player can move the cursor around the board and confirm the attack location. In reality this would be more appropriate for casting a ranged spell, because in general an attack should only be able to select an adjacent square (or basically just a direction), but this will serve well enough for now, and we already had that functionality available - we used it for the stride action.

I use LayerMask.GetMask to convert from a layer name, to a layer mask that represents the layer. This value will be passed to the Physics system to make it easier to find specific colliders.

Next I make a local variable for an optional Entity that is initially set to null. So long as the value is still null we will do a loop. The loop "awaits" the selection of a position. Then it will use our new Physics system to determine if an Entity is present at the specified location. Once an Entity has been found, the SelectTarget task will complete and return the Entity value.

Change the first two lines of the Perform method to the following:

var attacker = entity;
var target = await SelectTarget(entity);

I set the attacker to be the entity because it is the parameter that is passed that is performing the attack - even if it isn't their turn. I thought I had set that before, but I must have missed it somehow. Then I set the target to be the result of our new task to select a target.

Position Selection System

While I was reviewing the code in the position selection system, I noticed that it had OnEnable and OnDisable methods that it doesn't need. If it was a subclass of MonoBehaviour then it could handle its own injection, and it would make sense to keep those methods. I probably left them in by mistake from one of my previous prototype passes. Go ahead and open the script and delete them.

Combatant Assets

As part of helping illustrate how you could use physics to select targeted GameObjects, I decided I would put the Warrior and Rat prefabs in the "Hero" Layer. Those are the assets located within Assets -> Prefabs -> Combatants in the Heroes and Monsters subfolders. Make sure to make the same changes so that the code examples will work.

Demo

Play the game until you reach the Encounter. When it is the hero's turn, select the "Strike" action, then notice the selection indicator is present. You can use the arrow keys to select a location to attack, just like you would select a position to move had you chosen to "Stride". If you "confirm" on a tile where there is nothing to attack, then the selection indicator will just move back to the hero for you to try again. If you select a position with an Entity (even if that means you are targeting yourself) then the attack will be performed.

Summary

In this lesson we learned about obtaining an Entity based on having a GameObject reference. We also created a custom system to help wrap the Unity Physics engine so that we could do things like testing what objects were present at a given location.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *