In the Hackers group at CoderDojo Athenry, we have a small team of experienced young people who are in secondary school and have worked their way up through the other groups, developing their coding skills. This year, we are embarking … Continue reading →
This week we worked on our gem detector. We want a signal that behaves like this:
The signal strength should increase dramatically as we’re closer to a gem
The signal should be strongest when we’re pointing directly at a gem
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:
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):
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:
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:
We completed our Mario game this week. We coded Mario so that he always floated down on to the wall. We added a fraction of a second of a wait so that it appears that he floats as he comes down. This also allows time for you to navigate left or right as needed.
We also introduced a more advanced concept, the Parallax effect, whereby objects further away appear to move slower than objects nearer. We coded mountains and a Sun to demonstrate this.