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.