Programming and Spirals This week we combined 2 Scratch Projects: Introduction We did a lot of jumping around, from one project to the other, as we had to make changes to both projects to get everything working. Once everything was … Continue reading →
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:
A calculation to workout the top-left-back corner of the space covered by the ItemScatterer
A pair of loops which move us over the space in the X and Z directions
A calculation to workout the current position that we’re at on the top of the ItemScatterer
A probability check to determine if this is a location we should be placing an item
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:
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 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:
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:
The position to start the search from: thisPos which we calculated previously.
The direction to search in: Vector3.down.
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.
The distance to search: size.y – no point searching further than this
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.
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.
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:
Move the Terrain_2 GameObject out to the top-level of the hierarchy
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
Reset the transform on Terrain_2. As all other GameObjects are children, they move with it
Move all the children from under Terrain_2 back under the empties that originally held them
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:
The location that the ray starts from
The direction the ray points in
The maximum distance the ray can project
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:
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.
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:
A collider that acts like a solid barrier (standard collider)
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:
Both of them have to have collider components attached
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:
For physical collisions we implement OnCollsionEnter()
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:
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:
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:
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:
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!
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;
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:
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!
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 Name
Negative Button
Positive Button
Horizontal P1
a
d
Horizontal P2
left
right
Vertical P1
s
w
Vertical P2
down
up
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.
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 1
Input Option 2
Axis Value
W
Up-arrow
+1
<No key pressed>
<No key pressed>
0
S
Down-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:
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.
This week we continued our project from last week, looked at three main topics:
Frame rate independence
Creating our own properties on our components
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.
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);
}
}