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. 

Creators – Week 10

This week we continued the second week of our Gem Search project. We completed the ItemScatterer component by:

  • Completing the Distribute() function to scatter our items, temporarily lamp posts, across the environment
  • Writing a FindNearest() function to keep track of which item that we’ve spawned is closest to the player at all times

We then finally tested it by adding an ActivateNearest() function to light up the nearest lamp post and give us a visual validation that things are working.

Distribute

Our function to distribute the items comes in has five main pieces to it:

  1. A calculation to workout the top-left-back corner of the space covered by the ItemScatterer
  2. A pair of loops which move us over the space in the X and Z directions
  3. A calculation to workout the current position that we’re at on the top of the ItemScatterer
  4. A probability check to determine if this is a location we should be placing an item
  5. A Physics.Raycast() call which allows us to determine the exact height of the ground at this position

The Corner Calculation

The top-left-back corner, which we’re calling start, is calculated using the position of the ItemScatterer and the size that’s been specified:

        Vector3 start = new Vector3(transform.position.x - size.x / 2.0f,
                                    transform.position.y + size.y / 2.0f,
                                    transform.position.z - size.z / 2.0f);

The transform.position of ItemScatterer is going to be in the centre of its box. To get to the left and the back we take off half the box’s size in the X and Z directions respectively. To get to the top of the box, we add half the box’s size in the Y direction.

The Loops

The pair of loops look like this:

for (float xOffset = 0.0f; xOffset < size.x; xOffset += spacing)
{
    for (float zOffset = 0.0f; zOffset < size.z; zOffset += spacing)
    {
        ....
    }
}

The first loop, or outer loop, takes a variable called xOffset, initially zero, and keeps increasing it by spacing (default of 3) until it’s bigger than size.x. The represents us moving from the left to the right of the ItemScatterer.

The second loop, or inner loop, takes a variable called zOffset, initially zero, and keeps increasing it by spacing (default of 3) until it’s bigger than size.z. The represents us moving from the back to the front of the ItemScatterer.

The other important thing to note is that because the second loop is inside the first, for every step we make in X, we cover all positions in Z. Here’s what this looks like:

Calculating the Current Position

The calculation if the current position is relatively straightforward, the offsets in the X and Z directions are our loop variables and already calculate and we’re not changing the value of Y at all:

            Vector3 thisPos = start + new Vector3(xOffset, 0.0f, zOffset);
  

Probability Check

We already have our probability property for us to specify the chances of a location having an item. We compare this to Random.value, which is a random value between 0 and 1. If Random.Value is less than probability, then we’ll place something:

 // Check probability
 if (Random.value < probability)
 {
    // Place an item
 }

Physics Raycast

We have already placed our rocks and terrain into a layer named “Ground”. We only want to place our items on this layer. We can make sure that we only check this layer by using a LayerMask in our call to Physics.Raycast(). We add a new pubic string property to our class called groundLayerName and give it the default “Ground”:

public string groundLayerName = "Ground";

In our Distribute() function then, at the top before the loops, we can create the LayerMask:

LayerMask lm = LayerMask.GetMask(groundLayerName);

We also need to create a RayCastHit variable. This is somewhere that Physics.RayCast() can fill in details about what it hit. The complete code looks like this:

 // Check for ground
 RaycastHit hit = new RaycastHit();
 if (Physics.Raycast(thisPos, Vector3.down, out hit, size.y, lm))
 {
     Instantiate(item, hit.point, Quaternion.identity, transform);
 }

Physics.Raycast() returns a bool value (either true or false) and the if statement determines whether or not to Instantiate an item depending on whether or not ground was hit. The version of Physics.Raycast() we’re using takes the following inputs:

  1. The position to start the search from: thisPos which we calculated previously.
  2. The direction to search in: Vector3.down.
  3. The RaycastHit object that we want filled in: hit. Note the special out instruction that shows we’re going to getting values back from this – it’s not an input.
  4. The distance to search: size.y – no point searching further than this
  5. The LayerMask to use: lm which we calculated previously

The Instantiate is very standard, but we’ve also supplied our own transform as the parent transform. This means when the items are created, they are beneath ItemScatterer in the Heirarchy.

Find Nearest

For a human to scan a classroom and determine which desk is closest is something we do very quickly and intuitively, but a computer program generally has to take a more methodical approach. We look at each desk in turn and calculate how far away it is from us. If this is the first desk, or it’s closer than the previous desk we thought was the closest, we remember this desk and distance is is from is. We keep doing this until there are no more desks to compare against.

Here’s what the code to do this looks like:

   public Transform player;Q 
   public GameObject nearestItem;
    

    private void FindNearest()
    {
        GameObject foundNearest = null;
        float nearestDist = 0.0f;

        for (int childIndex = 0; childIndex < transform.childCount; childIndex++)
        {
            Transform thisChildTransform = transform.GetChild(childIndex);
            float thisDistToPlayer = (player.position - thisChildTransform.position).magnitude;

            if (foundNearest == null || thisDistToPlayer < nearestDist)
            {
                foundNearest = thisChildTransform.gameObject;
                nearestDist = thisDistToPlayer;
            }
        }

        nearestItem = foundNearest;
    }

We have two new public properties, one to hold the Transform of the player so we can know their position, and the other which will get assigned the nearestItem to the player every time it’s calculated. We call this FindNearest() function from Update() so that it gets run every frame.

Since we Instantiated our items as children of ItemScatterer, we can use transform.childCount and transform.GetChild(n) to know how many children we have and to get each one in turn, respectively.

Subtracting one Vector3 from another Vector3 returns a third Vector3 containing the displacement between them. Getting the magnitude of this returns the actual distance.

Finally our check:

if (foundNearest == null || thisDistToPlayer < nearestDist)
            

Says if we’ve never assigned a value to foundNearest, which will true the first time, OR (that is what the double-bars || means) the distance we just calculated is smaller than the previously smallest distance we knew about, then make this item the nearest item and remember its distance.

Code Download

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

Creators – Week 9

This week we started a new game called Gem Search. Here are some of the key design points:

  1. The player can wander freely in a low-poly 3D environment with a first-person perspective
  2. There are random gems hidden around the environment
  3. Gems come in four levels
  4. Each level has several variants of gem
  5. The player holds a detector in their hands; a gauge and an indicator light show proximity to a hidden gem
  6. When very close to a gem, the user can dig to recover it and it will be placed in their inventory
  7. Each gem has a score value proportional to its level and scarcity
  8. The detector can initially only detect the lowest level gems
  9. Gems can be combined to make higher level gems
  10. Gem combination recipes are randomised each play-through
  11. When combining gems the original gems are destroyed.,
  12. If gem combination is successful, a single gem of the next level is produced
  13. Once a gem of a higher level is owned, it can be installed in the detector; the detector can then detect gems of that level
  14. The player is against the clock to get the highest possible score

Creating a New Project

We started first by creating a new project called “Gem Search”, using the 3D core template.

Asset Store

We took two free assets from the Unity Asset store as a basis for our game:

We logged into the Asset Store using the same login we use for Unity, searched for the above assets and on the web page for each one, chose “Add to my assets”.

After that, we returned to Unity and opened the Package Manager from the Window menu. In the package manager we changed the option from “Packages: In Project” to “My Assets”.

Our two packages from the Asset Store were then visible. We downloaded and imported them both. For the Starter Assets – First Person Character Controller package, there was an import warning about switching to the new input system, which we accepted.

We then spent a little time examining the assets we’d acquired; both asset packs have demo scenes in them.

Setting up our Main Scene

We opened the LowPoly Environment Pack\Demo\Demo 3 scene and, after creating a Scenes folder at the top-level of our project, saved a a copy of this scene there as “Main”.

We then did a few adjustments. First we deleted the existing Main Camera. Immediately the fog in the scene was apparent (settings on the camera was suppressing it). We looked at where the fog settings were specified (select Window | Rendering | Lighting and then look on the Environment tab of this panel) and saw how it was causing a problem for the Scene View if we were zoomed out far enough to see the entire scene. We disabled the fog in the Scene View (it will still be visible in the game) from this drop down:

We noted that the entire terrain was offset by -2.6m vertically and rotated by 100% around the vertical. It woulds suit us better if it was square to the world and at (0, 0, 0). The difficulty was the if we moved or rotated the terrain, nothing would move with it and all the trees, bushes, rocks, etc. would end up misplaced. The solution that we followed was as follows:

  1. Move the Terrain_2 GameObject out to the top-level of the hierarchy
  2. Move every other GameObject with a mesh (but not the empties they were previously children of) to be a child of the Terrain_2 GameObject
  3. Reset the transform on Terrain_2. As all other GameObjects are children, they move with it
  4. Move all the children from under Terrain_2 back under the empties that originally held them
  5. Move Terrain_2 itself back inside “Environment”

Now the environment is still organised as before, but everything is neat and square.

Raycasting and Defining the Ground

The ground isn’t level and we are going to be looking for the height of the ground at any point we want to position a gem using the something called Physics.Raycast(). A ray-cast shoots an invisible ray looking to see if it hits a collider of any sort. We can control many things about this operation including:

  1. The location that the ray starts from
  2. The direction the ray points in
  3. The maximum distance the ray can project
  4. Which Layers to look at

All GameObjects start out in the “Default” layer, but it’s often handy to put things in specific layers when we want to be specific about them. This is one of those times. With any GameObject selected, we can add a layer by clicking the Layer drop-down at the upper right of the Inspector and choosing “Add Layer”. We took the first non-assigned Layer and called it “Ground”.

We then filtered the hierarchy to isolate those GameObjects with a MeshCollider by typing “t:MeshCollider” in the box at the top of the hierarchy. This showed that terrain and the rocks were the only ones; this is exactly what we want. We used Shift to select all of them and then changed them to layer “Ground”. We then cleared the box at the top of the Hierarchy to remove the filter.

Creating a Prefab for Testing

We made a Prefabs folder at the top-level of the project, right-clicked and choose Create | Prefab. We renamed this prefab Lamp Post and combined a plane, a cylinder and a lamp, using some materials that came from the LowPoly Environment Pack, into a simple lamp-like shape 2m tall:

Creating the Item Scatterer

We set out to create something that could scatter gems across our ground surface, comprised of the terrain and the rocks.

We made a scripts folder and created as new C# script there called ItemScatterer.cs.

Editing this we added a single property to start:

public Vector3 size;

This is to represent the size of the area over which we’ll be scattering our gems. It would be nice to be able to see this area. Unity allows you to draw Gizmos which show up in the Scene View (but never in the game). We add the following code:

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(transform.position, size);
    }

This is going to draw a yellow wireframe box at the position of the GameObject this component is attached to at the requested size.

We add an empty to our game, and call it “Item Scatterer”. We then attach our latest script to it. Setting the size to something like (150, 150, 150) we can see a large yellow box outlining our area. I move the area slightly to centre it closer to where the most trees are on the environment.

We progressed the scatter code a little, but I’ll cover it fully in next weeks notes.

Code Download

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