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. 

Creators – Week 8

This week we finished off our feeding animals game and played another challenge round. This concludes our second project and we’ll be starting on a brand new project after Christmas.

Collisions in Unity

There are two types of collider objects in Unity:

  1. A collider that acts like a solid barrier (standard collider)
  2. A collider that allows things to pass through it, but detects the collision (a trigger collider)

There is an “Is Trigger” check box on all collider components that allows us to change the type.

Standard colliders are used for physical interactions – like walls you can’t pass. Trigger colliders are used detect things being in the same space, but not physically interacting.

To detect collisions between two objects in Unity:

  1. Both of them have to have collider components attached
  2. At least one of them needs a RigidBody component attached

When Unity detects collisions, it sends messages to all components on the impacted GameObjects. We can choose to receive these messages by having the right functions in one (or more) of our components:

  1. For physical collisions we implement OnCollsionEnter()
  2. For trigger collisions we implement OnTriggerEnter()

These are similar but differ in information they receive.

Adding Collision to Our Prefabs

We selected each of the prefabs in our Prefabs folder and added a Box Collider to them. In the Game View we used the “Edit Bounding Volume” tool, as shown below, to adjust the Box Collider to make it a good fit.

To the Pizza Slice Prefab, we added a Rigid Body component and make sure to clear the option to “Use Gravity”.

We created a new script called DetectCollisions.cs and attached it to the animal prefabs. Editing this script we added the following function:

void OnTriggerEnter(Collider other)
{
  Destroy(gameObject);
}

Testing our code after this, we note that the animals disappear when they’re hit by a piece of pizza, but the pizza slices keep going. Once more change allows this code to remove the pizza slice as well:

void OnTriggerEnter(Collider other)
{
  Destroy(gameObject);
  Destroy(other.gameObject);
}

A Very Basic “Game Over”

We implement the most basic possible “Game Over’ message by updating the Update() function in DestroyOutOfBounds.cs as follows:

    void Update()
    {
        if (transform.position.z > topBound)
        {
            Destroy(gameObject);
        }
        else if (transform.position.z < lowerBound)
        {
            Destroy(gameObject);
            Debug.Log("Game Over!");
        }
    }

Now, when an animal reaches the bottom of the screen, the message “Game Over” gets printed to the Console window Unity.

Code Download

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

Creators – Week 7

This week we continued our Prototype 2 project.

Shooting Pizza

The first change we made was have it so that the pizza slices only emanated from the player when the space bar was pressed. We removed the temporary SpawnOnInterval component from the Player as this just spawned pizza slices continuously. We edited PlayerController.cs and added a new public property:

public GameObject projectilePrefab;

This has a type of GameObject which means it can store a reference to an object in the scene, or to a prefab, which is what we want. In the Inspector, we assigned our pizza slice prefab to this property.

In the Update() function in PlayerController.cs we added the following at the end of the function (just above the final curly brace that marks the end of the function):

        if (Input.GetKeyDown(KeyCode.Space))
        {
            // Launch a projectile from the player
            Instantiate(projectilePrefab, transform.position, transform.rotation);
        }

This checks to see if the space bar is pressed and, if it is, then it creates an “instance” (copy) of the pizza slice and places it at the player’s location with the player’s rotation.

Since the pizza slice already knows how to fly forwards, once it’s created it will automatically fly up the screen.

Creating Animal Prefabs

We have three animals in our scene. We checked to make sure that they had been rotated 180 degrees, so that the were pointing down the screen. We dragged each one from the Heirarchy to our Prefabs folder in turn, and then removed them from the scene.

In the prefabs folder, we selected them one at a time and added the MoveForwards.cs script in the Inspector, adjusting the speed from the default of 40 to a more sedate 5.

To test this, we ran the game with the Scene and Game views side-by-side. We could drag and drop animal prefabs into the Scene view and watch them run down the screen. Note that because we added them in run-mode, once the game was stopped, they were not left permanently in the scene.

Keeping Things Tidy

At the moment, any pizza slices and animals added to the scene are there for ever, long after they’ve moved off screen. In any game keeping stuff around that we don’t need any more is wasteful and might mean the game became ever slower as we continued to play it.

We create a new script in our Scripts folder called DestroyOutOfBounds.cs. We give it one private property:

private float topBound = 30;

This represents how far up something can travel in the game before we should delete it. Given our current game camera, up on the screen is equivalent to the global Z axis.

In Update() we add the following code:

        if (transform.position.z > topBound)
        {
            Destroy(gameObject);
        }

What does this say? It says: if we have moved such that our position in Z is greater than 30 (topBound) we should call Destroy(gameObject). The property “gameObject” hold a reference to the item in the scene that this component is attached to. Destroy() removes it from the scene.

We attach this new script to our pizza slice prefab and test it. Now pizza slices don’t stay in the scene forever, when they move to the top of the screen, they are removed.

What about the animals? We can use this same script for them as well. We add another private property:

   private float lowerBound = -10;

This represents how far down the screen things can move before they’re removed.

We change Update() so that it now looks like this:

        if (transform.position.z > topBound)
        {
            Destroy(gameObject);
        }
        else if (transform.position.z < lowerBound)
        {
            Destroy(gameObject);
        }

We have the earlier check for things going out the top, but now we have else if and another check for things going out the bottom. Note that with else if the second thing won’t be checked if the first thing turns our to be true, only when it isn’t.

Adding this now to our animal prefabs as well, we can see that when we drag them into the scene during runtime, they only last until the bottom of the screen.

Spawning Animals

We added a new empty object to the scene and called it “Spawn Manager”. We then created a new script file called SpawnManager.cs and attached it to this object.

In SpawnManager.cs we added a new property:

public GameObject[] animalPrefabs;

This property has the type GameObject, so it can store references to items in the scene, or prefabs. Note the square brackets after the type. This means this isn’t a normal property that stores a single value, this is an array and it can hold multiple values all at the same time. Looking at it in the Inspector, we find we can set its size to three and add all our animal prefabs to it:

We add a second property called animalIndex that will allow us to select which individual prefab we’re talking about at any one time:

public int animalIndex;

We can then add this code to Update():

if (Input.GetKeyDown(KeyCode.S))
{
  Instantiate( animalPrefabs[animalIndex],
                     new Vector3(0, 0, 20),
                     animalPrefabs[animalIndex].transform.rotation );
}

Every time we press the S key, an animal is spawned at the position of (0, 0, 20) (i.e. centre-top of the screen). By changing the value of animalIndex in the inspector between 0, 1 and 2 we can see different animals get spawned.

Random Animals in Random Places

We enhanced our SpawnManager.cs by randomising both the which animal is spawned and where it’s spawned. At the top of the class, we delete the animalIndex property and add these private properties instead:

    private float spawnRangeX = 20;
    private float spawnPosZ = 20;

The first we’ll use to determine the position of the animal horizontally, a random number between the left and right sides of the screen. The second will be the animals vertical position, and this will always be the same.

Inside Update() we change it to look like this:

int animalIndex = Random.Range(0, animalPrefabs.Length);
Vector3 spawnPos = new Vector3(Random.Range(-spawnRangeX, spawnRangeX),
                                       0, spawnPosZ);

if (Input.GetKeyDown(KeyCode.S))
{
  Instantiate( animalPrefabs[animalIndex],
                     spawnPos,
                     animalPrefabs[animalIndex].transform.rotation );
}

Now we’re setting animalIndex automatically every time to a random integer between 0 and up-to, but not including, animalPrefabs.Length. By using animalPrefabs.Length we can ensure this code works, regardless of how many animal prefabs we add to SpawnManager.

The position we’re also creating as a variable called spawnPos. The X value is random float value between -spawnRangeX and spawnRangeX. Y is aways zero and Z is always spawnPosZ. Note that down in Instantiate() we’re now using spawnPos and not the former new Vector3(0, 0, 20).

Automatic Spawning

The final change is automatic spawning. We move all the spawning code to a new function called “SpawnRandomAnimal”, where the check for the key press has been removed:

void SpawnRandomAnimal()
{
    int animalIndex = Random.Range(0, animalPrefabs.Length);
    Vector3 spawnPos = new Vector3(Random.Range(-spawnRangeX, spawnRangeX),
                                   0, spawnPosZ);

    Instantiate(animalPrefabs[animalIndex],
                spawnPos,
                animalPrefabs[animalIndex].transform.rotation);
}

Now all we need is to call this automatically. We create a new pair of private properties:

    private float spawnDelay = 2;
    private float spawnInterval = 1.5f;

and add this to the Start() function:

       InvokeRepeating("SpawnRandomAnimal", spawnDelay, spawnInterval);

This function will wait spawnDelay seconds, then call the named function for the first time. It will then call it repeatedly after that every spawnInterval seconds.

Now when we run, animals getting created constantly at random! No more need to press S.

And Finally…

The code for this week’s project is on our GitHub, as always. To download our stuff from GitHub, you can just click on the green button and choose “Download ZIP”

Note that this ZIP file will contain all our projects for the year!

Creators – Week 6

This week we started a new project, a top-down game where we’ll be feeding food to hungry animals who are rushing at us.

We had another asset bundle to download to get the assets we will be using for this project. It’s available on our Teams site.

Upon importing the asset bundle, we start with a basic scene which has three planes, two black ones flanking one with a grass texture. The asset bundle contains a total of four suitable textures for the ground and we had a look at those, and saw how to use them.

The asset pack also contains prefabs, these are pre-made combinations of Unity objects, ready to use. We had a look at some these prefabs by clicking once on them and viewing the preview at the bottom of the inspector. We also saw how double-clicking on them opens the prefab in the game view for editing and how using the back arrow at the upper-left returns us to the scene.

We brought in three animals, a human and a slice of pizza (scaled up by 3 or 4 to make it visible), laid out roughly as shown below:

Making the Player Move

We made a Scripts folder and added a new C# script called PlayerController.cs. At the top, we added two properties:

    public float horizontalInput;
    public float speed = 10.0f;

And Update() we changed to the following:

    void Update()
    {
        horizontalInput = Input.GetAxis("Horizontal");
        transform.Translate(Vector3.right * horizontalInput * Time.deltaTime * speed);
    }

Now, when testing, we see the player can go side-to-side, but it can also go completely off screen! How do we limit this? We do that by adding an if statement; this allows us to check if something is true, and then do something only when it is true.

First we added a new property to the top of the class:

    public float xRange = 10.0f;

This represents the range (between -10 and +10) that we want to confine the player’s position to.

In Update(), after the transform.Translate(), we add the following code:

       if (transform.position.x < -xRange)
        {
            transform.position = new Vector3(-xRange,  transform.position.y,  transform.position.z);
        }

What does this say? If the x part of the players position gets smaller than -xRange (-10) (meaning it goes too far to the left), then we change the player’s position so that the x part is exactly -xRange (-10).

When we test now, we’ll find that we can move as far right as we want, but when we move left, once the player’s x position gets to -10, we can’t move any further.

It’s easy now to add in the same check for the right-hand-side. We can copy-paste the last block of code and make a few small changes to the copy:

        if (transform.position.x > xRange)
        {
            transform.position = new Vector3(xRange, transform.position.y, transform.position.z);
        }

So, the less-than (<) changes to greater-than (>) and -xRange (-10) changes to xRange (+10) in two places.

Now the player is constricted in two directions.

Making a Moving Pizza Prefab

To make the pizza move we make a new C# script called MoveForward.cs and attach it to our pizza slice. In this script we have one property:

    public float speed = 40.0f;

And a single line in Update():

        transform.Translate(Vector3.forward * Time.deltaTime * speed); 

We make a new folder called Prefabs and then just drag our pizza slice from the Heirarchy into it. We now have a prefab of the moving pizza slice. We can delete the instance of the pizza slice in the scene; we’ll be spawning them automatically later.

And Finally…

We looked into spawning the prefab on a regular interval from the player’s position. That’s not part of the final game, but it was just to illustrate making an instance of a prefab from code.

The code for this week’s project is on our GitHub, as always. There is also small additional project there called “ScriptableObjects for a Recipe System” which is demonstrating a way to define ingredients and recipes and was in response one if our ninja’s queries. To download our stuff from GitHub, you can just click on the green button and choose “Download ZIP”

Note that this ZIP file will contain all our projects for the year!

Creators – Week 5

This week we turned our original scene, a truck driving down a straight road. into a two-player racing game around a curved track.

We deleted the road and crates from the old scene, imported a track asset from our Sharepoint site and moved our truck to be on it.

Fixing The Camera

The truck still drives, just as before, as it has the PlayerController component attached, but the camera, while it follows the truck, doesn’t behave as we’d like. It’s always looking in the same direction. We’d like it to point in the same direction as the truck itself.

The easiest way to achieve this is to remove the FollowPlayer script from the camera, and make it a child of the truck directly. We do this by dragging it onto the truck in the hierarchy:

We then select the camera, reset it’s transform component and tweak the Y (height), Z (distance back-and-forward along the truck) and X-rotation (tilt of the camera up and down) while keeping an eye on the camera preview (bottom right-hand corner of the game view) until we’re happy with the alignment.

Fixing the Controls

The controls on PlayerController are set to always use the input axes called “Horizontal” and “Vertical”. If we put two trucks in our scene now, they’d move together, even if one player was using the WASD keys and the other was using the arrow keys.

To fix this, we first update PlayerController.cs. We add two new public variables at the top:

    public string HorizontalAxis = "Horizontal";
    public string VerticalAxis = "Vertical";

And then down in the code where the “Horizontal” and “Vertical” were used explicitly, we replace them with the names of these two variables:

        // Get the player input
        horizontalInput = Input.GetAxis(HorizontalAxis);
        forwardInput = Input.GetAxis(VerticalAxis);

Everything behaves the same as before, but now, looking at the inspector, we can see that we can easily tell PlayerController to use other axes instead of these ones.

The Input Manager can be found under the Edit | Project Settings menu. In there are all the axes currently defined. Note that it’s not usual to see a few duplicates in this list; Unity will always pick up the first one matching the name it’s looking for and ignore the others. The number of axes defined is at the top. We increase the number by four (4) to allow us to make four new axes. In my case this meant changing the number from 18 to 22. Note that the four new axes created start out as copies of the last one in the list.

Open each of these last four axes in turn and name them “Horizontal P1”, “Horizontal P2”, “Vertical P1” and “Vertical P2”. We then need to set the negative button and positive button values on each of them like this:

Axis NameNegative ButtonPositive Button
Horizontal P1ad
Horizontal P2leftright
Vertical P1sw
Vertical P2downup

Close the Project Settings and select the truck. In the Inspector for PlayerController, change Horizontal Axis to “Horizontal P1” and change Vertical Axis to “Vertical P1”. Test the game, note that the truck now only moves in response to the WASD keys.

Fixing the Camera

The camera on the truck fills the screen when it draws, we want it to only draw to the left-hand side of the screen. To to this, select the camera and in the inspector change the Viewport Rect settings like this:

Look at the Game view, the camera’s only drawing to the left hand side of the screen, this is because we’ve set W (meaning width) to a half.

Adding the Second Player

Select the truck in the hierarchy, right-click and chose “Duplicate”. This makes a copy of the truck, including all its components and children. It doesn’t look like that though because they’re right on top of each other. Separate them by moving the copy a bit. I suggest using the overhead view for this.

Rename the trucks “Player 1” and “Player 2”.

We just need to tweak “Player 2” a little bit. Select it and change the PlayerController to use the axes “Horizontal P1” and “Horizontal P2”. Then select it’s camera and change the Viewport Rect making X = 0.5. This means that not only is it half width, but it also starts drawing at half way across the screen,

Play the game. Player 1 should draw on the left side of the screen and be controlled by the WASD keys. Player 2 draws on the right and is controlled by the arrow keys.

Code for This Week

Updated code is on our GitHub repo: https://github.com/coderdojoathenry/Creators-2022. It also includes some camera switching code that I briefly demonstrated, but was too complex to finish in time.

Creators – Week 4

This week we took a pre-made scene with some problems and set out to fix them.

The first two issues were that the plane was flying backwards and at great speed. The initial challenge was to identify what was making the plane move. Looking at the plane, called Player in the scene, in the inspector, we could see it has a script component called Player Controller X added to it.

Double-clicking on the name of the script file (PlayerControllerX.cs) in the inspector allows us to open it. The code for moving the plane is in the Update() method:

        // get the user's vertical input
        verticalInput = Input.GetAxis("Vertical");

        // move the plane forward at a constant rate
        transform.Translate(Vector3.back * speed);

        // tilt the plane up/down based on up/down arrow keys
        transform.Rotate(Vector3.right * rotationSpeed * Time.deltaTime);

We need to concentrate on the line that starts transform.Translate(…). The comment above it says “move the plane forward”, but the line itself is specifying Vector3.back as the direction of movement. Changing this to Vector3.forward makes it move in the right direction. The plane is still much too fast though. This line is asking the plane to move the distance specified by the speed parameter (by default 15m) every frame. What we need to do is to multiply here by Time.deltaTime, the amount of time since the last frame, to convert this movement into 15m per second, not per frame. Here’s the corrected line:

        // move the plane forward at a constant rate
        transform.Translate(Vector3.forward * Time.deltaTime * speed);

The next problem is that the plane turns on its own. In fact, although we’re gathering the value of the “Vertical” input axis in the code, pressing the arrow keys does nothing. We can identify the line of code that’s making the plane turn:

        // tilt the plane up/down based on up/down arrow keys
        transform.Rotate(Vector3.right * rotationSpeed * Time.deltaTime);

This is asking the plane to rotate around Vector3.right (horizontally through the plane) by the number of degrees specified by the property rotationSpeed (with a default value of 100) every second. The multiplication of Time.deltaTIme is what makes it “every second” and not “every frame”, as before. So the plane is turning 100degrees every second, making it do a full loop approximately every three and a half second.

Nowhere here are the user inputs taken into account. How can we use them? Remember that the “Vertical” input axis in Unity works like this:

Input Option 1Input Option 2Axis Value
WUp-arrow+1
<No key pressed><No key pressed>0
SDown-arrow-1

We’re gathering this axis value into a property called verticalInput already, we just need to use it. When multiplied in, it works like a switch. If its off (having the value zero) then no rotation happens. If it’s one or minus one, rotation happens in either a positive or negative direction:

        // tilt the plane up/down based on up/down arrow keys
        transform.Rotate(Vector3.right * rotationSpeed * Time.deltaTime * verticalInput);

Now the plane flys correctly and responds to user input, but the it flies directly into the camera and then can’t be seen any more. First we need to grab the camera and move it to the side of the plane, rotating it around to point at the plane. This is better, but it still doesn’t move. The plane quickly flys beyond the area the camera can see. If we look at the camera in the inspector, we can see that it already has a script called Follow Player X on it:

It has a property called Plane, but nothing’s assigned there. We can set this by dragging and dropping the plane from the hierarchy, or by clicking the small circle icon on the right above and picking the “Player” object from the pop-up list. Running and testing this shows that the camera now follows the plane, but it’s right on top of the plane. It needs to be some distance away. Examining the FollowPlayerX.cs script we see at the top there’s a private property called offset:

    private Vector3 offset;

that is used in the Update() method already:

    transform.position = plane.transform.position + offset;

We just have to give it a value. The easiest way is to change private to public and then specify a value for the offset in the inspector.

The final challenge is to turn the plane’s propellor. Looking in the inspector, we can see this is its own object, a child of the “Player” object:

To make it spin, we can make a simple new script called SpinPropellor.cs and attach it to the propeller. This code works well:

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

public class SpinPropellor : MonoBehaviour
{
    public float spinSpeed;

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

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(Vector3.forward * Time.deltaTime * spinSpeed);
    }
}

This is very similar to what we’ve done before, but the axis of rotation is the Vector3.forward here to make the propellor spin in the expected way. I found a value of 1000 or more was good for spinSpeed.

Note that although we’re moving the propellor ourselves, or at least rotating it, that doesn’t interfere with the fact that it’s moving with the plane, because it’s a child object.

Code for this Week

Updated code is on our GitHub repo: https://github.com/coderdojoathenry/Creators-2022 The problems with projects not loading correctly when downloaded has been corrected.

Creators – Week 3

This week we continued our project from last week, looked at three main topics:

  1. Frame rate independence
  2. Creating our own properties on our components
  3. The Input Manager and getting and using user input

Frame Rate Independence

We covered how to make our actions independent of frame-rate. The code:

transform.Translate(Vector3.forward, 20.0f);

moves the object 20m in every frame. As we know, frame-rate isn’t generally consistent between machines. On my powerful laptop, I was getting up to 1400 frames per second at fastest. Most ninjas were getting a few hundred frames per second on theirs.

On the other hand, the code here:

transform.Translate(Vector3.forward, Time.deltaTime * 20.0f);

moves the object at a consistent 20m per second on everyone’s machine. The magic is that Time.deltaTime variable. It is the time, in seconds, since the last frame was drawn. The faster the frame-rate, the smaller this number gets and the result is a consistent 20m per second movement.

Creating Properties on our Components

We can add properties, variables where we can hold and values that we can then use, to our classes by typing a single line into our class definition:

class MyBehaviour : MonoBehaviour
{
  // The new property
  public float myProperty = 1.0f;  

  void Start()
  {
  }
  void Update()
  {
  }
}

Looking at the bits of the line in turn:

  • public – The access modifier. Can be private, internal (which we won’t use) or public. If it’s public we can see it in the inspector, and from other classes. If private we can only see and use it within the class itself.
  • float – The type of value we are storing. A float is a number with a decimal point. An int (short for integer) is a number without one. We might use an int for counting things, but we use floats for real world measures like speeds and positions, etc.
  • myProperty – the name of the variable
  • = 1.0f – Assigning a default value to this property. This portion is optional. the ‘f’ after the number is just a hint to the computer that this is a float value.
  • ; – The standard semicolon to end the line of code.

The Input Manager and Using User Input

Unity’s Input Manager contains the definition of input “Axes”. These can contain many ways of doing the same thing.

The default definition of the “Horizontal” axis means it can be triggered by the keys A and D, or the Left and Right arrow keys or by the joysticks or d-pad on a game controller.

This means input is “abstracted”; we can write our script to respond to input, without worrying how that input is generated.

To get the value of input on an access we need to use code like this:

forwardInput = Input.GetAxis("Vertical");

Here we’re getting the value of the axis called “Vertical” and storing it in a variables called forwardInput.

The value of the vertical axis goes between -1 and 1. Minus one means fully down, zero means no input and one means fully up. Because of this range, we can use this value like a switch, multiply it with other numbers. When there’s no input, it’s zero which will zero out the expression it’s part of.

Here’s the fully updated code for our PlayerController.cs:

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

public class PlayerController : MonoBehaviour
{
    public float speed = 20.0f;
    public float turnSpeed = 50.0f;
    private float horizontalInput;
    private float forwardInput;

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

    // Update is called once per frame
    void Update()
    {
        // Get the player input
        horizontalInput = Input.GetAxis("Horizontal");
        forwardInput = Input.GetAxis("Vertical");

        //  Move our vehicle forward
        transform.Translate(Vector3.forward * Time.deltaTime *
                            speed * forwardInput);

        // Rotate our vehicle 
        transform.Rotate(Vector3.up, turnSpeed * Time.deltaTime *
                         horizontalInput);
    }
}

Code for this Week

Updated code is on our GitHub repo: https://github.com/coderdojoathenry/Creators-2022

Creators – Week 2

Hi folks, this week we started our first project. We have a truck that can drive down a road, avoiding obstacles, or maybe not!

We created a new project in Unity, using the default 3D Core template, and called it Prototype 1.

We then downloaded and imported the assetpack from here: [Creators Teams Channel]

The asset pack already included an existing scene, which had a simple environment already. We them dragged in a vehicle and obstacle from the imported assets. Imported assets aren’t just models; they can contain Unity obstacles, such as colliders, already.

To make the truck move, we made a new C# script called PlayerController. The new C# files Unity creates always look the same (apart from the name of the Class, which matches the new file name):

We added the following code to the Update() method to change the transform of the vehicle:

    // Update is called once per frame
    void Update()
    {
        //  Move our vehicle forward
        transform.Translate(0, 0, 1);
    }

The Unity scripting documentation can be found:

https://docs.unity3d.com/ScriptReference/index.html

and the specific page for the Transform component is:

https://docs.unity3d.com/ScriptReference/Transform.html.

This method on the Transform component that we’re calling, Translate() has several forms. The one we’re using here expects us to provide X, Y, Z values. What we’re saying we want to happen is “Change the transform by moving it 1m in Z every frame.

When we run, the car moves very fast off the end of the road. That’s because we’re running at many frames a second. It’s too fast. Next week, we’ll look at making this frame rate independent and controlling the speed.

Finally, I’ve created a GitHub repo for our projects this year. Up-to-date versions of our projects will always be available here after our sessions: https://github.com/coderdojoathenry/Creators-2022