Creators – Week 17

This week we:

  • Imported updated gem images
  • Imported an asset pack containing our new Inventory UI and a prefab to serve as an inventory slot
  • Worked on the code for the inventory slot

Updated Gem Images

We downloaded GemImages.zip from our Teams site. Importing these files directly into Unity wouldn’t work. Unity associates an identifier (which is a large random number) with assets in a project. This identifier is commonly called a GUID or Globally Unique Identifier. This allows us to rename and move things and any links we’ve established remain in place.

GUIDs aren’t absolutely guaranteed to be unique, but the chances of two being the same is 1 in 360 undecillion. What’s an undecillion? It’s a 1 followed by 36 zeros. Not just huge, but unbelievably vast.

If we import new assets, they’ll get new GUIDs. Anything we linked to the old versions will remained linked to the old versions. What can we do?

Well we can get around this by updating the files outside of Unity. If we replace the files in Windows Explorer or Mac Finder, then Unity sees they’ve updated, but considers them still the same asset they were before and all links remain good.

You can see, when looking in Windows Explorer or Mac Finder that for every file and folder in a Unity project there is also a file of the same name but with the additional ending .meta. This file stores all the asset properties from the Inspector window and, surprise surprise, the GUIDs as well.

New UI Asset

We downloaded the GemInventoryUI.assetpack from our Teams site and imported it into Unity. There were warned that we were going to overwrite existing assets and change their GUIDs. This happened because we’d previously imported frame3_distort.png and frame3_distort_hollow.png, which are in this asset pack as well. Every single one of us ended up with a different GUID for these assets.

When this happens you have two choices:

  1. Import these assets from the asset pack and break any links that exist in your project already
  2. Don’t import these assets from the asset pack and break any links with items within the asset pack

In this case, the choice is easy. We only used frame3_distort.png once in our project and we haven’t used frame3_distort_hollow.png yet at all. In the asset pack, in contrast, we’ve used these two assets several times. We choose the first option and we just have to edit our Gem Found prefab to fix the link to frame3_distort_hollow.png in the Panel control that’s now broken.

We also changed the colour of all the “Header” text in our new Inventory Canvas prefab from Palatinate Blue to Cherry Blossom Pink. This is a nicer balance of colour and retains the strong Palatinate Blue colour for the button, emphasising that it’s an action.

Finally, we dragged a copy of this Inventory Canvas prefab into our Main Scene. It can’t be used right now, because the First Person Controller has the mouse locked to itself to control the look direction, so we just disabled it for the time being.

Inventory Slot – Starting to Work on the Prefab

To work on the Inventory Slot we created a new scene called Inventory Slot Test. We set it up as follows:

  • Added a Canvas
  • Added a Panel to the canvas. It’s automatically set to stretch to it’s parent, filling the canvas.
  • Set the Panel’s background to anything other than white and set it to opaque (A = 255)
  • Add another Panel as a child of the Panel above. Call it “Slot Holder”
  • Change Slot Holder’s background to white and opaque.
  • Changed Slot Holder’s Anchor and Pivot: Click on the button in the Rect Transform component. While holding the SHIFT and ALT keys, select top-left.
  • Now that the anchor’s changed, the size and position can be set. Set the size to 100×100 and move Slot Holder so it’s somewhere near the middle of the left-hand side of the screen.
  • Duplicate Slot Holder and move the copy to the middle of the right-hand side of the screen.
  • Drag a copy of the Inventory Slot component as a child of both Slot Holder and Slot Holder (1)

Your screen should look similar to this when done:

We create a new script called InventorySlot.cs in our Scripts folder. Edit the Inventory Slot prefab and add the script to the prefab.

Note that it’s easy to make these changes to the copy of the prefab in the scene, but that’s not what we want. We want to edit the prefab itself so that every copy of the prefab, including those in the Inventory Canvas, get the changes.

Updating GemDefinition.cs

We take a little detour here to update GemDefinition.cs. We need a Sprite so we can attach it to an image control, etc. Gem Definition has a Texture2D. Similar but not the same. We can make it so that Gem Definition makes a sprite from this Texture2D the first time we ask for it, and holds onto that Sprite anytime we ask for it again. Here’s the updated code:

[CreateAssetMenu(menuName = "Gem Search/Gem Definition")]
public class GemDefinition : ScriptableObject
{
    public int Level;
    public int Value;
    public GameObject Prefab;
    public Texture2D Icon;

    private Sprite _sprite;

    public Sprite Sprite
    {
        get
        {
            if (_sprite == null)
            {
                _sprite = Sprite.Create(Icon,  new Rect(0, 0, Icon.width, Icon.height), new Vector2(0.5f, 0.5f));
            }

            return _sprite;
        }
    }
}

The property Sprite is not like any we’ve seen before. It actually has code that runs when we access it under get. We could also have code that runs when it’s set (under set unsurprisingly). Since we’ve specified get here and not set this property is read-only; that’s often a useful thing.

Since this new-style property can’t actually store anything on it’s own, there’s a private property called _sprite that actually does the storing of the value. When someone asks for GemDefinition.Sprite, it checks to see if it’s been made before. If it has, it just returns it. If it hasn’t it makes it and then returns it.

Inventory Slot – Showing the Gem and the Count

Now that we have a Sprite we can use, let’s start plugging this together. Here’s the code for InventorySlot.cs:

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class InventorySlot : MonoBehaviour
{
  public GemDefinition GemDefinition;
  public int GemCount;

  public Image SlotImage;
  public GameObject CountTextBadge;
  public TMP_Text CountText;

  // Start is called before the first frame update
  void Start()
  {
  }

  // Update is called once per frame
  void Update()
  {
    if (GemDefinition == null || GemCount < 1)
    {
      SlotImage.sprite = null;
    }
    else
    {
      SlotImage.sprite = GemDefinition.Sprite;
    }

    CountTextBadge.SetActive(GemDefinition != null && GemCount > 1);
    CountText.text = GemCount.ToString();
  }
}

We edited the Inventory Slot prefab and assigned the SlotImage, CountTextBadge and CountText to the corresponding items in the prefab (Content Image, Item Count Badge and Count Text respectively).

We then ran our scene. We can see that when we assign GemDefinition to the slot and set the count to 1 or higher, the image and the count badge behave as expected.

Inventory Slot – Adding Interfaces for Drag and Drop

We need Unity to send messages to our InventorySlot component when dragging and dropping is occurring, but how to we do that? Well, if our component implements certain Interfaces then it says to Unity, I’m interested in Drag and Drop.

An Interface in C# is just a contract that a class will have certain properties and functions. To things outside the class it means they can now access the class through these properties and functions, confident that they exist. It’s up to the class itself to actually make sense out of the properties and functions so that something appropriate happens!

First we need to add a new using to InventorySlot.cs. This is where the Interfaces we’re interested in are defined:

using UnityEngine.EventSystems;

Now we update the class definition to include the Interfaces we want to support:

public class InventorySlot : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler, IDropHandler

If you are using Visual Studio, you should find that these Interfaces each have a wavy red underline because their contract is not fulfilled. You should be able to right-click on each one, choose “Quick Actions and Refactorings” and “Implement Interface” to get it to fill in the interfaces for you automatically. You will see four new functions, each with a throw line that makes an error happen if the function is called. This is to force you to put your own code in. Just delete these throw lines are you’re left with:

  public void OnBeginDrag(PointerEventData eventData)
  {
  }

  public void OnDrag(PointerEventData eventData)
  {
  }

  public void OnDrop(PointerEventData eventData)
  {
  }

  public void OnEndDrag(PointerEventData eventData)
  {
  }

Inventory Slot – Begin Dragging

To allow dragging, we want to create a new image on the canvas and move it with the mouse. First we add these two private properties to InventorySlot.cs:

  private Canvas _canvas;
  private GameObject _draggingIcon;

In Start() we then set _canvas by looking for one in our parents:

  void Start()
  {
    _canvas = GetComponentInParent<Canvas>();
  }

We update OnBeginDrag() and OnDrag() as follows:

public void OnBeginDrag(PointerEventData eventData)
  {
    if (GemDefinition == null || GemCount < 1)
      return;

    _draggingIcon = new GameObject("Icon");
    _draggingIcon.transform.SetParent(_canvas.transform, false);
    _draggingIcon.transform.SetAsLastSibling();

    var image = _draggingIcon.AddComponent<Image>();
    image.sprite = GemDefinition.Sprite;

    OnDrag(eventData);
  }

  public void OnDrag(PointerEventData eventData)
  {
    if (_draggingIcon == null)
      return;

    RectTransform iconRt = _draggingIcon.GetComponent<RectTransform>();
    RectTransform canvasRt = _canvas.GetComponent<RectTransform>();
    
    Vector3 globalMousePos;
    if (RectTransformUtility.ScreenPointToWorldPointInRectangle(canvasRt,
                                                                eventData.position,
                                                                eventData.pressEventCamera,
                                                                out globalMousePos))
    {
      iconRt.position = globalMousePos;
    }
  }

Now when we run and set a GemDefinition and Count of one or higher on our slot, if we drag from that slot with the mouse, we get an icon that shows the gem and moves with the mouse. Once we stop dragging it remains in place.

This is most of drag and drop done; we just need to make a few tweaks next session: control the size of the drag icon, make it semi transparent and make sure it doesn’t block the mouse interactions and remove it up when we stop dragging.

Code Download

The code for this week’s project is on our GitHub, as always. 

Creators – Week 16

This week, we worked to make the “Gem Found” prefab that we created last week actually appear when we found a gem, and disappear after it’s played it’s animation.

A Script for Our ‘Gem Found’ Prefab

We want a simple script, called GemFoundMessage.cs, that will set the message on the prefab’s canvas to match the gem we just found:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class GemFoundMessage : MonoBehaviour
{
    public TMP_Text Text;
    public Transform GemHolder;

    public void SetGemDefinition(GemDefinition gd)
    {
        string message = string.Format("You found a {0} spirit gem!",
                                       gd.name);
        Text.text = message;
    }
}

We attach this to the root of of our prefab. This script contains a single function we can call to set the text based on the gem definition and a couple of properties. One property is the text we need to update, the other is the location where we’re going to spawn a gem model to have it animate.

Making the GemFound Prefab Clean Itself Up

We already have a SelfDestruct script. We add it to our GemFound Prefab and give it a time of 4 seconds; this is enough time for our animation to finish.

Updating PlayerDig

We now need to update PlayerDig.cs. There are a few things to do:

  1. After spawning our digging animation, we want to wait three seconds and then check to see if we’ve found a gem.
  2. We look to see if the nearest gem is within a certain distance (the larger we make this the less precise the player will need to be)
  3. Assuming there is a gem there, find out what type of a gem it is
  4. Add it to our inventory [We’ll need to wait to do this, as we don’t have our inventory written yet]
  5. Create a GemFound prefab
    • Inform it what gem type we have
    • Create a gem as part of the GemFound

First we add the line Invoke(“DigResult”, 3); this to the OnDig() function as shown:

        :
        if (Physics.Raycast(rayPos, Vector3.down, out hitInfo, 10.0f, lm))
        {
            Instantiate(DiggingPrefab, hitInfo.point,
                        DiggingPrefab.transform.rotation);
            Invoke("DigResult", 3);
        }
        :

We then add the DigResult() function:

    private void DigResult()
    {
        float distToNearest = (ItemScatterer.nearestItem.transform.position -
                               transform.position).magnitude;

        if (distToNearest > MaxDistToItem)
            return;

        // Capture the gem definition
        RandomGem rg = ItemScatterer.nearestItem.GetComponent<RandomGem>();
        GemDefinition gd = rg.GemDefinition;

        // Remove the item from ItemScatterer
        Destroy(ItemScatterer.nearestItem);
        ItemScatterer.nearestItem = null;

        // TODO: Add to our inventory

        // Spawn the Gem Found Prefab to let the player know what they found
        GameObject gfb = Instantiate(GemFoundPrefab, GemFoundLocation);
        GemFoundMessage gfm = gfb.GetComponent<GemFoundMessage>();
        GameObject gem = Instantiate(gd.Prefab, gfm.GemHolder);
        gfm.SetGemDefinition(gd);
    }

This does everything we talked about above.

Basic UI

We took a short look at simple UI layout options as preparation for building the UI next session.

Code Download

The code for this week’s project is on our GitHub, as always. 

Creators – Week 15

This week we established a palette for our UI design, brought in a custom font, a custom UI background sprite and build a prefab to use when we find a gem that contained an animation.

Hexadecimal

Before we talk palettes, it’s good idea to introduce a new counting system called hexadecimal. The counting system we usually use is called decimal and it has ten digits, from 0 to 9. Hexadecimal is similar, but it has 16 digits. Counting in hexadecimal looks like this:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, ....

So 10 (say one-zero, not ten!) written in hexadecimal is 16 in decimal.

The main reason we use hexadecimal when working with computers is that it aligns neatly with the way information is stored inside the computer itself. A byte or 8-bit value can always be written with two hexadecimal digits, a 16-bit value with four hexadecimal digits and so on.

Palette

There are combinations of colours that are inherently more pleasing to our eyes and there are rules that allow them to be calculated. There are lots of websites that will calculate a colour palette for you. We chose https://coolors.co/.

You can see that the colour are represented by six hexadecimal digits. This is three bytes (0-255 in decimal, 0-FF in hexadecimal) representing the red, green and blue components of the colour. It’s a tidy way to represent it and easy to share and reuse.

This is the palette I chose for our game:

It’s intended to be bright and cheerful and include some colours with a high contrast with each other.

In Unity we can add these colours to a custom palette, so we can recall them quickly any time we want. At the bottom of any colour-picker dialog you can find the option to create a “Swatch Library”

We can then add the current colour to the library by selecting the last button under “Swatches”.

Custom Font

We wanted a nice font for our game. The right choice really helps set a mode. I choose a font called Brady Bunch Remastered that’s free for non-commercial use. I found it here https://www.dafont.com/brady-bunch.font

In Unity we saved this file to a new folder called Fonts. We then needed to set it up so that Text Mesh Pro could use it. Under the Window | TextMeshPro we used the Font Asset Creator to build a “font atlas” from the BradBunR.ttf font file.

Custom UI Background

The standard UI background in Unity is very plain and boring. I created an image to give our UI elements more character.

We imported this into Unity to our Sprites folder. We then needed to set a few things to allow Unity to use it in a UI. In the Inspector we changed the “Texture Type” to “Sprite (2D and UI)” and “Mesh Type” to “Full Rect”. We then hit the “Apply” button near the bottom of the Inspector to apply these changes.

Unity allows you to set borders on our sprites so that when they’re scaled, the corners don’t change, but the pieces in between stretch instead. This preserves the appearance when the sprite is used as the background to differently sized elements. To specify this we need to first add the 2D Sprite package to Unity and then use the “Sprite Editor” button in the Inspector to open it. We specified 36 for all borders. The hand-drawn blue arrows show how the different regions will stretch when used:

Prefab for when we Find a Gem

We developed a prefab to use when we find a gem. It’s intended to be spawned inside a child of the main camera that places it just in front of the player.

The prefab contains a Canvas. Canvases usually overlay the game screen, but they can be worldspace canvases, that actually exist as objects inside the scene. As Canvases are always huge, we needed to specify a very small scale to bring it down to something that makes sense.

We create a script GemFoundMessage.cs and attach it to the root of our prefab. It’s used to take information about gem was found and use that to update text on the Canvas and to spawn in a copy of the gem (which we can animate).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class GemFoundMessage : MonoBehaviour
{
    public TMP_Text Text;
    public Transform GemHolder;

    public void SetGemDefinition(GemDefinition gd)
    {
        string message = string.Format("You found a {0} spirit gem!",
                                       gd.name);
        Text.text = message;

        Instantiate(gd.Prefab,
                    Vector3.zero,
                    gd.Prefab.transform.rotation,
                    GemHolder);
    }
}

Animation

To animate in Unity, we first open the Animation window from the Window | Animation menu. It works well if docked at the bottom of the screen where the Console usually is.

We then select the object that we want to animate, and press the Create button in the Animation window.

This makes an Animation Clip and Animator Controller in our project and attaches an Animator component to our GameObject.

With our object selected, we press the “Add Property” (1) button to select the properties we want to animate, we then click in the timeline (2) to select the time at which we want to specify the property value and finally we edit the property value at that time (3).

Code Download

The code for this week’s project is on our GitHub, as always. 

Creators – Week 13

This week we imported some assets for our gems, distributed random gems around the environment and created a digging action and effect.

Importing Gems

To avoid the tedious work of getting up all the gem definitions, I created an asset pack on our Teams site containing:

  1. Gem models
  2. Gem images
  3. Completed gem definitions
  4. Updated GemDefinitions.cs

To avoid clashes, we deleted our existing gem definitions and GemDefinitions.cs script before importing the asset pack.

Random Gem

We wanted to create a prefab which represented a random gem. First though, we need to get the gem definitions into memory. We create new class called GemManager.cs and attach it to a new object in the scene, also called ‘Gem Manager’. GemManager.cs has an array for storing all the gem definitions and a single function to return one at random:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GemManager : MonoBehaviour
{
    public GemDefinition[] GemDefinitions;

    public GemDefinition RandomDefinition()
    {
        int index = Random.Range(0, GemDefinitions.Length);
        return GemDefinitions[index];
    }
}

In the scene we set the size of the array to 15 and added all our gem definitions to it.

We then created our RandomGem.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RandomGem : MonoBehaviour
{
    public GemDefinition GemDefinition;

    // Start is called before the first frame update
    void Start()
    {
        GemManager gm = FindObjectOfType<GemManager>();

        if (gm != null)
        {
            GemDefinition = gm.RandomDefinition();
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.white;
        Gizmos.DrawWireSphere(transform.position, 0.5f);
    }
}

This class looks for a GemManager in the scene when it starts and, assuming it does, it asks it for a random gem definition and stores that in its GemDefinition property. We also added an OnDrawGizmos() so we could see, in the Scene View, where the gems have been scattered.

Updating ItemScatterer

All we needed to do was to set Item in the to new random gem prefab and removing ActivateNearest(), as RandomGem doesn’t have Activate()/Deactivate() functions, which our lamp post did.

Adding a New Player Action: Digging

To see the existing user actions that had been defined, we navigated to the Player Capsule object and looked at the PlayerInput component. It references an asset called StarterAsset which, when we double-click on it, opens a new editor window. We use the “+” button next to Actions to defined a new action called “Dig”. Next to “Dig” we use the “+” to defined two new bindings; one to the ‘X’ key on the keyboard and one to the “Button West” button on the gamepad.

To respond to this new action, we create a new script called PlayerDig.cs, containing a function called OnDig(), and attached it to the “Player Capsule” game object – it has to be on the same game object as the PlayerInput component. Here’s the code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerDig : MonoBehaviour
{
    public void OnDig()
    {
        Debug.Log("Player hit dig");
    }
}

In Play mode, we can see now that every time we hit the “X” key the message “Player hit dig” is written to the console.

Creating a Dust Particle Effect

On our Teams site there is a small image file called “dustmote_alpha.png”. We added it to our project in the ‘Sprites’ folder. Selecting it, we updated the properties in the inspector and selected “Alpha is transparency” (to indicate that the image has transparent portions) and hit the “Apply” button to save the change.

We created a new material, in our “Materials” folder, called “Dust Mote”. We needed to change the rendering mode to “Cutout” and assign our “dustmote_alpha.png” image to the texture box for the Albedo channnel:

We then created a new prefab with a particle effect on it. The settings are too numerous to list here, but you should examine it in the project if interested. Our material above provides the appearance of the particles.

We also added an audio source to this prefab and downloaded a digging sound effect from our Teams site to attach to it.

Finally we developed a small class called SelfDestruct.cs and attached it to the prefab:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SelfDestruct : MonoBehaviour
{
    public float SelfDestructInSeconds;

    // Start is called before the first frame update
    void Start()
    {
        Destroy(gameObject, SelfDestructInSeconds);   
    }
}

This little component means that we can create the dust effect and it will clean itself up after the sound and the particle effect have finished (about 3 seconds).

Code Download

The code for this week’s project is on our GitHub, as always. 

Creators – Week 12

Importing an FBX

We downloaded the Gem Detector FBX from from our Teams channel and imported it into Unity, in a new folder called ‘Models’. In the Inspector, we used the “Extract Textures” and “Extract Materials” options to take the textures and materials stored inside the FBX and place them into our project as items we could edit. When extracting the textures, Unity warned that two were Normal Maps and offered to mark them as such for us, which we accepted.

What is a Normal Map?

A normal map is just a texture file. It is used to cheaply, from the point of view of rendering time, provide fine detail in models. The little video below illustrates this. There are two square plates, side-by-side.

The plate on the left has been modelled to have a circular feature that rises up in a lip and then back down again. It uses many vertices and polygons to model this.

The plate on the right is just a single quad, but it has a normal map applied to it. How was this normal map created? It was “baked” from the plate on the left. This plate is always flat, but it reacts to light as if it isn’t.

See how they both behave very similarly when a light is passed over them. It isn’t until the camera tilts down to a very low angle that the illusion breaks and the plate on the right starts to look a little odd. The video shows the wireframes at the end so you can see the difference in the two plates from a geometry standpoint.

Adding A Light and Sound To Our Detector

To light up the detector, we need two elements. A point light to cast light on the rest of the model and an emissive material we can switch to to make the glass cover on the bulb look lit-up.

With our extracted materials and textures, we now had a material called ‘Light Cover’ in our ‘Models’ folder. This is used for the lamp cover at the top of the sensor. It’s supposed to be transparent, so we selected that material and in the Inspector changed the Rendering Mode setting from Opaque to Transparent and we then opened the Albedo colour and reduced the A value (which stands for Alpha and is the degree of transparency) down to about 175.

We then duplicated this material and renamed this copy as ‘Light Cover (On)’. We edited this one to enable emission by clicking on the Emission check box, then opening the Emission Colour dialog and setting the colour to a red colour and finally pressing +2 at the bottom of that dialog once to brighten the colour.

Then we added a new object, a point light, as a child of the “Light Cover” object in the detector and manoeuvred it into position.

Finally, we downloaded a sound file, of a beep sound, from our Teams channel to an ‘Audio’ folder in our project and set it as the AudioClip for an AudioSource component, with Play On Wake disabled, attached to our Light Cover object.

Script for Turning on the Light and Sounding A Beep

Here is the code for turning on and off the light and swapping the material on the light cover:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SensorLight : MonoBehaviour
{
  public Material MaterialOff;
  public Material MaterialOn;
  
  private bool _isOn = false;
  private MeshRenderer _mr;
  private AudioSource _as;
  private Light _bulb;

  // Start is called before the first frame update
  void Start()
  {
    _mr = GetComponent<MeshRenderer>();
    _as = GetComponent<AudioSource>();
    _bulb = GetComponentInChildren<Light>();
  }

  // Update is called once per frame
  void Update()
  {
    if (_isOn)
    {
      _mr.material = MaterialOn;
      _bulb.enabled = true;
    }
    else
    {
      _mr.material = MaterialOff;
      _bulb.enabled = false;
    }
  }

  public void TurnOn()
  {
    _as.Play();
    _isOn = true;
  }

  public void TurnOff()
  {
    _isOn = false;
  }
}

Note that it uses GetComponent<T>() and GetComponentInChildren<T>() to find the different components we need to interact with, rather than having them as public property we’d have to set manually. This approach can be a little easier to maintain.

Connecting it all to the Gem Sensor

We downloaded a finished version of the Gem Sensor ProximitySensor.cs script from our Teams channel. Very much based on last week’s code, but with the addition of logic to work out the flashing light’s timing.

The final thing was a very simple class to tie the ProximitySensor and the ItemScatterer together:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LinkScatterToSensor : MonoBehaviour
{
    public ItemScatterer ItemScatter;
    public ProximitySensor ProximitySensor;

    // Update is called once per frame
    void Update()
    {
        if (ItemScatter.nearestItem != null)
        {
            ProximitySensor.Target = ItemScatter.nearestItem.transform;
        }

    }
}

Code Download

The code for this week’s project is on our GitHub, as always. 

Creators – Week 11

This week we worked on our gem detector. We want a signal that behaves like this:

  1. The signal strength should increase dramatically as we’re closer to a gem
  2. The signal should be strongest when we’re pointing directly at a gem
  3. The signal should be zero when the gem is behind us

Setting Up a Test Scene

To develop the sensor, we created a new test scene. It consists of a flat plane, a tall and narrow cube to represent our target (signal source) and the prefab containing the first person character controller.

We also added a new script ProximitySensor.cs to our Scripts folder and attached it to an empty object in our new scene as “Proximity Sensor”.

Adding UI Elements to the Scene

We added a few Text – TextMeshPro objects to our scene. Adding the first one automatically added a Canvas object as well. In the scene view. the UI elements look enormous, but in the game view, they just overlay the game image.

Calculating Distance

All of our positions in Unity are stored as Vector3 objects. If you subtract one Vector3 from another the result is also a Vector3 that contains the differences between the two positions. Unity can give us the length of this (or any) Vector3 through it’s magnitude property. The code below shows this calculation and us setting the value of the UI Text element so we can see it:

float distance = (Player.position - Target.position).magnitude;
DistanceTxt.text = "Distance: " + distance.ToString();

Calculating Base Signal Strength

We want a scheme were by the signal falls off quickly with distance. We start with a nominal strongest signal value of 100. If we divide this by distance it falls off, but not that strongly, as shown in the blue line below. If instead we divide it by distance squared (meaning the distance multiplied by itself) the impact of distance is much more dramatic and the value falls off to a small value much more quickly. This is the red line.

The problem with this scheme is that once distance goes less than 1, the resulting value is greater than 100 and when we get to zero, the value hits infinity (anything divided by zero is infinity).

We can simply solve this infinity problem by adding one to d2. This means that the smallest value it can have is 1, not zero and the highest value the signal strength can get to is 100.

This seems perfect, but actually in testing its clear we can never practically get to zero distance, so we never get to full signal strength. The final modification is not to add one to d2 but to add something a bit smaller. We’re going to use 0.9. Now the signal strength will go as high as 100/0.9 = 111.11 so we just use logic that says if it’s greater than 100, then make it 100. Here’s what that looks like (noting the flat section at the start):

Here’s what the code matching this looks like:

float signalStrength = MaxSignalStrength / ((distance * distance) + 0.9f);
signalStrength = Mathf.Min(signalStrength, MaxSignalStrength);
SignalStrengthTxt.text = "Signal Strength: " + signalStrength.ToString();

Unit Vectors/Normalised

A Vector3 that has a length of one is called a “unit vector”. Vector3.normalised gives us back a version of that Vector3 that points in the same direction as the original by always has a length of one.

Pointing In The Same Direction

If we have two Vector3s that have a length of one, we can use the Vector3.Dot() function to determine to what degree they’re pointing in the same direction. Vector3.Dot() also called a Dot Product. When we pass Vector3.Dot() two unit vectors it will return a value between -1 and 1. If the two vectors are pointing in the exact same direction, it will be 1 and if they’re pointing in opposite directions it will be -1. If it’s zero it means that they are perpendicular to each other and don’t point in the same direction at all. Any positive value in-between means they’re pointing in the same direction somewhat.

We can work out the Vector3 from the Player to the Target and the Player’s Forward vector and compare them using Vector3.Dot(). We will make it so that all negative values go to zero. The resulting value is one that gets close to 1 when the player is pointing directly at the target. Here’s the code:

Vector3 playerToTarget = (Target.position - Player.position).normalized;
float pointingTowards = Vector3.Dot(playerToTarget, Player.forward);
pointingTowards = Mathf.Max(pointingTowards, 0.0f);

Combining Distance and Direction

To combine these calculations, we just multiply them. Now we have a measure that reacts to both distance and direction. Here’s that code:

float adjustedSignalStrength = signalStrength * pointingTowards;

Code Download

The code for this week’s project is on our GitHub, as always.