Hi, my name is Mark. For years I've wanted to make my very own video games, using software
like Unity. Unity is the powerful game engine behind titles like Cuphead, Neon White, Tunic,
Outer Wilds, Hearthstone, Firewatch, and even the Pokemon Diamond remake.
But I've always found that lengthy, multi-part, meandering tutorials just send me to sleep. I
can't learn by watching someone else - I have to get hands-on and figure things out for myself.
And so last year I developed a solution that actually works. It's a three-step technique
where you: one just learn the absolute basics of Unity. Then, two, cement those lessons with simple
exercises. And then, three, figure out the rest as you go along. And it totally worked! In the space
of about a year, I went from ripping off iPhone games to working on my very own puzzle platformer
about magnets. And I released an interactive video essay that's had over 100,000 plays.
But wait, I hear you say! How do you do step one? How do you learn the basics,
when the software is so complicated to figure out?
Well for me it was about writing down a list of things I would need to know,
regardless of what game I was going to make. Things like how to make a character appear and
move them around the screen. How to make stuff spawn in and then delete it again
later. How to have collisions and game over and animations and sound effects.
Then I learned all that by hunting through lengthy tutorials, reading the Unity docs,
Googling esoteric words, and doing a lot of trial
and error. And so the whole point of this video is to save you that hassle.
This video is the tutorial I wish I had when I was learning Unity.
So in the next 40 minutes we're going to use the engine to make Flappy Bird. Not because we want
to make Flappy Bird, but because in order to remake this addictive iPhone game, we'll need
to learn basically everything I just listed, from spawning objects to getting game overs.
This tutorial will cover every step of the way from downloading Unity, to understanding the UI,
to writing your very first line of programming code, to building a game that you can share with
your friends. And then, when the tutorial is over, I'll share some concrete next steps that you can
take in order to continue learning the rest by yourself. Sound good? Then let's get started.
Okay, let's start by getting Unity from the website. Download and install the Unity Hub.
And then you'll need to make a free account to actually use it. Once that's done,
you'll be asked to install the Unity Editor - I'm using version 2021.3 for this tutorial,
if you're watching a million years in the future and wondering why
things are different. Let's pretend I have fast internet - Neeooowwwwmm.
We're not quite done yet. Under installs, hit the cog icon on the Unity Editor and pick modules.
You'll see that Microsoft Visual Studio has been ticked - this is the software we'll use
to write programming code. So hit continue. And install Visual Studio. On this screen,
scroll down and tick game development with Unity, and untick Unity Hub,
because we already have it. Neeooowwwwmm. We don't need to make an account to use Visual Studio,
so skip that. And don't bother loading it, we'll open it later.
Okay, that's all done now. So in Unity Hub, pick new project. Choose all templates. And use 2D,
Core. This is an empty project, with a few configurations to make it suitable
for 2D games. Give your project a name, hit create, and let's get game makin'.
In step one, we're going to become familiar with the default Unity user interface. And
as we explore the different panels, we'll make the bird appear on screen.
Right. So this is the default screen layout for Unity, and it's split into four panels.
First of all, down here, is the Project panel. This will contain everything that is in our game
- like sprites, sound effects, scripts, tiles, fonts, and so on. Some of this stuff will be made
in Unity as we go along. But we can also just drag and drop files from elsewhere on our computer.
Like, I've made some sprites for the bird and the pipe in Photoshop and I'm going to
import them into my project like so. I'd recommend you make your own - that's always more
fun - but if you have zero artistic ability then check the description for these assets.
The next panel is the hierarchy. This contains all of the stuff that's in the current scene - which,
in most games, will be a level. We're going to start by making the bird,
so right click and choose Create Empty. This has made an empty GameObject... so what's that?
Well, a GameObject is essentially an invisible container. It has a position in space, a rotation,
and a scale. Then, you can fill that container with components - to add extra features. For
example, if we add a Sprite Renderer component, we can slap the bird image onto the GameObject.
Absolutely everything in our level will be a GameObject with components - the bird,
the pipes, even the user interface and the camera.
All of this magic happens in the third panel, the Inspector - which is for messing with GameObjects.
So, once we've selected our new, empty GameObject we can put a name in the top field - let's call it
Bird. And we can see and change the GameObject's position, rotation, and scale, under Transform.
We can now press Add Component, pick Rendering, and pick Sprite Renderer. To make this work,
we need to fill in the sprite field - so just drag the bird image from the
project panel into the field and viola, we have graphics!
That will, of course, show up in the fourth and final panel,
the scene view. Here we can see what's in our current scene, and,
if you want, you can use these tools to move stuff around, scale it, and so on.
This section has an extra tab for game view, which shows us what the game will look like from
the main camera when it's running. Also, from this dropdown, we can set a resolution or aspect ratio
to get a better idea of what it will look like when played - so I'm going to choose 1920 by 1080.
Oof, the bird takes up way too much space. We could scale it down, but let's actually
just zoom out the camera. Like I said before, the camera itself is a GameObject in the hierarchy.
And it has a camera component with stats we can mess with. By changing the size, we can zoom out.
I'm also going to change the background colour. Lovely.
We can now press the play button up here to start... the world's most
boring game. Okay, let's make it a bit more exciting.
A quick recap. Unity has four panels by default. Project holds all the stuff in our
game. Hierarchy lists all of the GameObjects in the current level. Inspector lets us see
and change those GameObjects. And we can see the level in the scene view.
And a GameObject is an invisible container that we can fill with components, like a sprite renderer.
In step two we're going to use more components to make the bird into a
physics object that is affected by gravity. And then we're going to
write some programming code to make the bird fly up when we press the space bar.
So let's add another component to our bird: a Rigidbody 2D. This turns our bird into a
physics object, with gravity. So when we hit play, the bird drops, and falls off the screen. Cool.
We'll also want this bird to be able to interact with other objects, so let's add a collider.
A circle collider 2D.
Back in scene view we can see the collider as a green outline. It's a bit off-center for me,
so I'll use the offset to move it. And, a little game design trick - if we make the
collider a bit smaller than the image, it will let the player get through pipes even if they
juuust touched the edge. It gives the game a bit of leniency and makes it feel more fair.
The final thing to add right now: a script. This essentially lets us make our own custom
component - but we'll have to write it ourself using programming code. Choose New Script from
the components list. And call it BirdScript. Once it's loaded, double click the script
field to open it up. This will open the file in Visual Studio, which we installed earlier.
So, welcome to programming! It's not too scary,
promise. We'll take it slow. We're writing in C sharp, that's the programming language.
And the only thing to worry about right now is these two chunks here: start and update.
Start is for any code that will run as soon as this script is enabled. And it
runs precisely once. Update runs constantly while the script is enabled. And it will fire
off every line of code, every single frame. Over and over and over again.
So the main thing we're going to be doing with code right now is - well, if we go back
to Unity - see these numbers and text fields in the components? And how we can change them in
the Unity editor? We're just going to write code to change these stats while the game is running.
Just as a dumb example, and we'll delete this in a second. In start,
we can type gameObject - that refers to this bit up here. And then a dot. You'll see a list appear,
and many of the items refer to stuff back in the Inspector, like isStatic,
tag, layer, and name. So let's pick name. Then write an equals sign. And in quotes,
give our bird a name. Finally, we must always use a semi-colon to mark the end of a command.
And we must always save the script before we go back to Unity.
Now, when we run the game... the name of the GameObject has been changed. Nice.
Okay, delete that code. That was just for sillies - but it shows us how we
can use code to talk to the game. We can write a command by choosing someone to
talk to - in this game, the GameObject - and then a topic of conversation - its
name - and then a command - change it to Bob Birdington. We'll be doing this a lot.
So what we actually want to do is... in the Rigidbody 2D's component, under info,
we'll see a greyed-out field for velocity. And we want to
write some code to add upward velocity to the bird to make it fly into the air.
The problem is... initially, a script can only talk to the GameObject's top bit and
the transform. Right now, this script is completely unaware of the other components.
So we need to sort that out first. We need to make a special slot on this script for
a Rigidbody2D - so we can then talk to it and send it commands. This is called a reference.
We're going to create the reference up here, between the class name and
the start function. We're going to write public Rigidbody2D myRigidbody.
So we now have a slot to store a Rigidbody2D. And we have a name that we can refer to - to make sure
we're talking about this specific Rigidbody2D. And because we made it public, it means we can access
this slot from outside the script. So, if we save. And go back to Unity, we'll see that the script
component now has a field for a Rigidbody2D. We can drag the component into that slot, and viola.
We have established a line of communication between the script and the Rigidbody.
Okay, back in Visual Studio. In update, we can type myRigidbody. Then dot. And
now look at all the things we can talk about. Angular drag, gravity scale,
mass - these are all properties on the component. The one we want is velocity.
We want to set this to a new number, and so, just like before with the name, we'll write an equals.
Now what we're actually writing here is a vector, which is two numbers, to represent a position in
2D space. And in this case, it's used to represent a direction for the bird to travel. We want the
bird to go straight up, so zero, comma one would be a good one. I'm just going to use Vector2.up,
which a built-in shorthand for zero comma one. And to give it a bit more power, I'm going
to multiply that vector by a number. Say, 10, which should send the bird flying up in the sky.
Now, like I said before, any code in update will run, over and over again,
every frame. So if we save the script and hit play in Unity... off goes our bird. Bye!!
That's not what we want. We want this to only happen when the player hits the space bar. So
it's time to use the most fundamental bit of programming code: the if statement.
An if statement is like a gate.
You can surround some code with a fence, and every frame that code will be completely
ignored. Unless, the game meets some specific conditions that are written
on the gate - in which case the gate is open, and the code is read and executed.
So we want to say "if the player hits the space bar, then add upward velocity".
To do this... we can write if, and then in brackets we can write the condition. This time
we're not talking to a component, we're talking to Unity itself - specifically its input system. So
we'll write Input. Then we can pick GetKeyDown, and in brackets, KeyCode.Space. This asks Unity
if the space bar has been pressed on this frame. And then we'll finish with equals, equals true.
A quick note on equals signs - we use one to make the thing on the left be the same
as the thing on the right. And we use two if we're just checking if
the thing on the left is the same as the thing on the right. Cool?
Anyway. So this code says... if the space bar has just been pressed, then... and then we'll
use curly brackets - these are the fence in our little analogy - and put the flap code in here.
So, now in update - every frame the game will go to the gate and be asked "hey,
has the spacebar just been pressed?" If yes, the code will fire and the bird will flap. If not,
it will skip the code in the curly brackets and try again next frame.
So - save the script and go back to Unity. We can now hit play and tada:
the bird goes up when we press space. We have now
created a character and made it react to input. This is a video game. Hooray!
However, it feels like trash. The flap isn't right, and it doesn't feel like the original
iPhone game. So we could change this number. Save. Open Unity. Run the game. Not quite right. Stop. Change
the number. Save. But that's slow and dumb. Let's do something smarter.
First, we're going to make a variable. Let's go back to the top of the script and under
our reference to the Rigidbody, let's make a public float called flapstrength. A float
is a floating point number - basically a number that can have a decimal place. And
then back in our update code, we'll multiply the vector2.up by flapstrength, instead of 10.
Now, back in Unity, you'll see that the script component has a new field:
flapStrength. And we can change that whenever we want to make the game feel
different. We can even change it during the game, but note that anything you change while
the game is running won't save when you press stop. This means you can play with
values to your heart's content without worrying about screwing up your game.
So, if we mess with the flapStrength, and also the gravity scale on
the Rigidbody, we'll hopefully get to something that feels good. Ah,
changing numbers back and forth: honey, that's game design!
Recap time.
We can use code to change the properties of a component, while the game is running.
A script cannot talk to the other components on the gameobject, by default. You have to
make a line of communication by storing a reference to that specific component.
We create the reference in code, and then fill it in Unity by dragging and dropping.
Code in start runs once, when the script comes into existence. Code in update runs continuously,
every single frame. But, we can use if statements to skip some code, unless a condition is met.
And we can use public variables to change certain
values in Unity's inspector - even while the game is running.
Okay, so the secret to Flappy Bird is that while it looks like a bird
is flapping along through a world of pipes - it's actually not. The bird
stays completely still and the pipes move across the screen. So in step three we're
going to make pipes spawn into the world, move across the screen, and then delete themselves.
We'll start by making the object we want to spawn. This will be
two pipes which move across the screen, from the left to right.
Let's make another GameObject called pipe. Put it exactly on the bird for now,
to get the sizing right. And then we'll make another object within this one,
called top pipe. This is a child of the first GameObject's parent. This way we
can nest multiple GameObjects, and move all of them at once just by moving the parent.
So let's repeat what we did for the bird. Add a sprite renderer for the pipe image.
And add a collider - a Box Collider 2D, this time. We don't need a RigidBody because it's not going
to be affected by physics. We can then move it up above the bird - but keep the X position as zero.
Finally, we can duplicate this whole top pipe object. Call it bottom pipe. And flip
it upside down by changing the Y scale to minus one. Then move it down below the bird.
As you can see, if we mess with the pipe parent GameObject, both pipes move, scale,
and rotate along with it, with the parent as the pivot point. So let's
add a script to this parent's object to make it move across the screen.
We'll start by creating a variable for moveSpeed. If we give it a number here,
it will fill this as the default value in Unity. But we can always change it there, later.
Then we'll write code to move the object, in update. Now it would be lovely if we could just
type transform.position.x, and change this number directly - but, no, boo, you have to change the
entire Vector in one go. Oh, and this time we're gonna have to use Vector3, instead of Vector2,
because the transform has three numbers. Even though we're making our game in 2D, Unity is still
fundamentally a 3D engine and so it's keeping track of the object's depth with the Z value.
So, here's what we'll do. We'll take the current transform.position. And then equals.
We want to add to its current position, so write transform.position again. And
then plus. And finally, in brackets, we'll do Vector3.left * moveSpeed.
Back in Unity, press play and vroooof. That's way too fast. Now,
you might think that you could just change this moveSpeed variable down to a really
small number like 0.001. And that will work - but that's not actually the problem here.
You see, code in update just runs as often as it can. In fact, if we check the stats in Game view,
we'll see the game is running at over 1,000 frames per second. Heh,
sorry PlayStation 5. 120 fps? Pfft, that's got nothing on Flappy Bird.
And the real problem is that the game may run at different speeds on different computers,
and we don't want the pipe to move faster or slower depending on your rig.
Real games have actually made this mistake - in Dark Souls 2, weapon durability was once tied
to frame rate, so your swords would break twice as fast at 60 FPS, compared to 30 FPS. That was a whoopsie.
Luckily, it's a pretty easy fix. We just multiply it by Time.deltaTime.
This ensures the multiplication happens the same, no matter the frame rate. We didn't
need it for the velocity code because physics runs on its own little clock,
but otherwise we will need it. if you want to know more - about this, or anything really,
the Unity docs are a good place to check. You'll find info and sample code.
Okay, now with that fix in place, our pipe moves smoothly across the screen. Lovely.
Next, we want to create a system that will continually spawn new pipes. To start,
take the parent GameObject from the hierarchy and drag it into your project. This creates a
prefabricated GameObject. Or prefab. This is like a blueprint for a GameObject and we can create new
versions of this entire GameObject- with all its children, components, and properties. Oh,
and before we move on, we can delete the original in our hierarchy now. Bye bye.
Let's make a new GameObject called Pipe Spawner.
We'll put it just to the right of the camera. And we'll make a script for it. The purpose of this
script is to spawn new versions of the pipe prefab every few seconds. And because the pipe already
has code to move left, the pipe will automatically move across the screen as soon as it spawns in.
We're going to write some code to spawn that prefab we just made. So
we'll start by making a reference to the prefab.
Up here, we'll type Public GameObject pipe.
Then in Unity, we'll use the same drag and drop method to fill the slot, but this time,
instead of a component, we'll drag the prefab from the project panel.
Now, Unity has a nice built-in method for spawning
new GameObjects. We'll type Instantiate, and then open the brackets. In here, the command is asking
for some extra details. we can actually flip through these to find different, I dunno, recipes?
I guess? Number 4 looks good - it will create an object at a specified position and rotation.
So, for the GameObject, we can type pipe. For position
we can just type transform.position to get the position of the object holding
this script. That will make it spawn on top of the spawner. And for rotation,
let's just use transform.rotation so, again, it's the same as the spawner.
Let's run it and oh my god, that's not what we want. Spawning works great,
but they're coming out every single frame - and we want them to come
out on a nice interval that we can control. So, back to Visual Studio.
What we're going to do now is to write some code to make a timer. this will count up for
a specified number of seconds, run some code, and then start the count again. To do this,
we'll need to make a couple variables. A spawnRate is how many seconds it should be
between spawns. And then a timer is the number that counts up. We can make this
one private as we won't be changing it in the editor or anywhere else.
In update, we'll do another if statement. This time, if the timer is less than the spawnRate,
then we want to make the timer count up by one. So we'll take the timer as it currently is,
and add Time.deltaTime to it. This creates a number that counts up every frame,
and works the same no matter what your computer's frame rate is.
We can actually shorten this by changing it to +=, but, don't feel like you need to make your code as
short as humanly possible just to avoid getting sniffy YouTube comments. If
timer = timer + is easier to read and grasp, then that's absolutely fine. You
can always swap to the other version in the future when you feel more confident.
Now, before I said an if statement is like a gate.
And we can add another gate to the side of it, with else. This means,
if the condition isn't met, then skip the code - and do the code in else, instead.
So we'll put the spawn code in here, and also reset the timer to zero. So now, every frame, it
asks if the timer is less than the spawn rate. If it is, then count the timer up. If it's not - i.e.
the timer has actually met or exceeded the spawn rate, then spawn a pipe and start the timer again.
Put this in Unity and - pretty good. I'm happy with that. The only problem is... we have to
wait ages for the first pipe to spawn. It would be good if this came out immediately, right?
Now, we could copy and paste the spawn code into start, so it happens once in start.
And then happens over and over in update. But that's a bad idea. You should generally try to
avoid having the same, or even similar code in multiple places. What happens if we want
to change how the spawn works? We'll have to find and change it everywhere. No good.
Instead, we can put the spawn code in a new function, and then just run that
function. So here, below update - but above the final curly bracket - we'll
make a function called void spawnPipe(). And then cut and paste the Instantiate code into there.
Now we can just write spawnPipe, with empty brackets, in both update and start. This will run
all the code in that function when these lines are executed. And with that done, it will make
a pipe as soon as the game begins, and will make new pipes every time the timer maxes out. Perfect.
However - this is a pretty boring game, right? The pipes always come out in the middle. we want them
to come out at random heights. So, remember that when we wrote the instantiate code,
we had to pick a position for the object to appear? We'll change that value.
Right now the pipes always spawn on the same position as the spawner. We want the X value
to be the same... but for Y, we want to pick a random point somewhere above or below the spawner.
So let's create a public variable for a heightOffset, maybe 10.
And then we'll make a float called lowestPoint. Because we're making this variable inside the
function, rather than at the top of the script, it means it can only be used within the function. But,
also, it means we can set it by doing a calculation.
so we'll do equals transform.position.y - heightOffset. And then we'll make another
one for highestPoint, but this time it's plus heightOffset. That gets us these two numbers.
Then we'll replace the transform.position in our Instantiate code.
We're gonna write new Vector3, we have to write that whenever we're specifying our own numbers for a
vector. and then in brackets we'll specify the X, Y, and Z values as three different floats. For X,
we want this to be the same as the spawner, so we'll do transform.position.x. But for Y,
we can do Random.Range. And in the brackets for that, we can supply a minimum and maximum point
to pick from. That's lowestPoint and highestPoint. Then a 0 for Z. And close the brackets.
Back in Unity.... nice! The pipes will spawn anywhere between these two numbers.
Oh, one last thing. Every time these pipes spawn they'll appear and move
left.... forever. Which isn't great practice - they're off screen and doing absolutely nothing,
and yet they're still in memory and running code every frame. And if too many spawn they'll start
to spill out the side of your monitor and make a right mess of your desk. So let's fix that.
Now we could make a timer, and delete the pipe after a few seconds. But instead,
we'll check the X position of the pipe, and delete it if it goes past a certain point. We'll borrow
the bird to find out the X coordinate of the left of the screen. Looks about minus 45. In
the pipe move script, we'll add a float for a deadzone. -45. And then a simple if statement - if
transform.position.x is less than deadZone, then destroy the GameObject that holds this script.
Run it in Unity and, bam, they're dead.
Let's do one more thing, just as a teachable moment. Just before the destroy line,
let's write Debug.Log, and in brackets, Pipe Deleted. Then, back in Unity, you'll see one other panel
I skipped during the UI demo - it's a tab next to project, called console. Then when we run the
game... every time a pipe is deleted, our message is sent to the console. This is a
wonderfully useful way to debug our code, because we can find out exactly what the code is up to.
Recap time!
GameObjects can be turned into prefabs,
by dragging them from the hierarchy, and dropping them into the project.
You can then drag these into scenes - I use prefabs to create levels in my puzzle game,
for example. Or you can make a spawner to instantiate new ones during the game.
Timers are a great way to make code happen on a certain interval,
but always use Time.deltaTime to keep things consistent across different computers.
If statements can have an else gate, to make code fire if the condition
is not met. You can also have else if, to make more complicated gates.
And you should try to delete GameObjects if they're no longer needed, to free up memory.
Okay, our next step is to keep track of the player's score,
and show it to the player on the user interface. Then,
we want the score to go up by one, every time the bird goes through the pipes.
So, remember that a GameObject doesn't have to be a physical thing in your game
world like a character or an enemy - it can be a completely invisible manager
that's just keeping track of critical data like health, or time, or score. And then,
we can make that information visible to the player, using a user interface.
So let's start by making the UI. Like everything else, it's a GameObject in
the hierarchy. This time go down to UI and pick text - which may be under
legacy. We'll need to zoom really far out on the scene view to actually see the UI.
To make sure the UI looks the same on every device, we'll pick this new canvas GameObject
and set the canvas scaler component's UI scale to scale with screen size, and choose a sensible
reference resolution - I'm gonna use 1080p again. We can then move our text around. You'll notice
that UI has a rect transform, rather than a normal transform. The most important thing to note is
that you don't really want to mess with scale of elements - instead, change the width and height.
I'll then increase the font size and set the default
text to 0. And then check it all looks nice on the game view.
Okay, now we want to make a script that will store the player's score,
and change the number on the UI to that score.
We'll make a GameObject called Logic Manager.
And we'll give it a script. This script is going to keep track of high level information
like the player's score. And it will have various meta-level functions that we can run.
So we'll delete start and update, we don't need them in this script. We can always add
them back later if we change our mind. We want to store a number for the player's score. This time,
we don't want a float because we only ever want round numbers. So let's do an int,
instead. That's an integer. No decimal places.
And because we want to update the UI text we just made we will, as always,
have to make a reference. Except... text doesn't seem to be a thing?
Ah, well. By default, a script only loads in the stuff you need for basic Unity
functionality - but if we go up to the top and type using UnityEngine.UI;, we can now
access more functionality - in this case, UI stuff. Now we can make a reference to text.
We'll need to drag the text component into this field back in Unity. Because we're referencing a component
on another GameObject - the text on the UI - the best way to do this is to just drag
the whole GameObject into our slot. This will automatically find the text component for us. Handy.
So now we want to make a function. And we'll call it addScore. And because
we're going to run this function from other scripts, we'll set it to public void.
This function needs to do two things. Add one to the player's score. Easy enough,
we know how to do that now. And change the text on the UI to be this number. Oh, the text box is
looking for a string - a sequences of characters - and our score is an integer. They look identical
to us humans, but robots are fussy. Easily fixed, mind you, by adding .toString() to the game score.
To make sure this works, let's give ourselves the power to run this function
from Unity itself. All we need to do is write ContextMenu, and a name, above the function.
Now, in Unity, while the game is running, hit the little dots on this script and pick the function.
Nice! This sort of thing comes in real handy for testing.
Okay, so now that we know the function runs, we specifically want to run it
when the bird goes between the pipes. And the way to do this is collisions.
Now if two objects have colliders, they will bash into each other - in fact, in our game,
the bird will already crash into the pipes because we've added colliders to both. However - you
can also have invisible colliders, called triggers. They don't create an actual collision,
but they do let you know that two objects have touched - and you can run code at that moment.
So we're going to put a trigger in between the pipes,
so we know that the bird has passed through them. And then at that moment, we'll run addScore.
Let's open up the prefab for the pipes. We'll make another GameObject called middle - and
it needs a box collider. Let's make it this sort of shape. And this time we'll
tick the box isTrigger. Finally, let's add a script to this new middle GameObject.
Beneath Update, type ontrig, and the autocorrect will help us type out OnTriggerEnter2D. Just
press tab to autofill. Anything in this function will run whenever an object first
hits the trigger. There's also OnTriggerExit and OnTriggerStay, for future reference. And its in here,
that we want to run the addscore function we wrote earlier... except. ah. once again,
this script doesn't know about any other scripts in the game, until we make a reference to it.
So we can write public LogicScript logic. But back in Unity, you'll quickly realise
that you can't drag the script into this slot. You can't drag it from the project
panel - we can only talk to an instance of a script that lives on a GameObject. But we also
can't drag from the scene into the prefab. That's because the pipe doesn't exist in the scene yet,
it will only exist when the game is running, and the spawner starts making pipes.
So, instead, we'll need to fill this reference using code. and
this needs to happen when the pipe first spawns.
To do this, we'll need to help the code find the logic script. To do this,
take the Game Logic object, and look at the top of the inspector:
you'll see tags. From the drop down, choose add tag. Make a new tag called, say,
Logic. And make sure you go back to the GameObject and actually set this new tag. You will forget to
do this approximately eight thousand times in your Unity career, so look forward to that.
Now, back in the PipeMiddleScript,
under start we can write logic = GameObject.FindGameObjectWithTag("Logic").
this will look for the first GameObject in the hierarchy with the tag,
Logic. In our case, there will only ever be one in the scene,
so we know it will always find the right one - but do be mindful of that.
And then we can add .GetComponent();
So, as soon as a new pipe spawns, it will look through the hierarchy to find a GameObject with
the tag Logic. Then, it will look through that object's components to find a script
of the class LogicScript. And if it finds one, it will put that in our reference slot.
It has done the exact same thing as dragging and dropping the component
in the Unity editor - except it has done it instantly, during run time. Excellent.
So now, the pipe's middle script can find and talk to the logic script.
And if we write logic.addScore, this will run that code. Back in Unity,
hit play and if we did everything right, the score will go up by one when we pass between the pipes.
Oh, and just for future proofing and whatnot, let's make sure that it was actually the bird
that went through. We'll do this by putting the bird on a layer, and checking if the colliding
object was on that layer. Go to the bird's GameObject and this time, instead of the tag, we'll change
the bird's layer. Make a new one, remember to actually assign it, and make a note of the number.
Now, on the pipe's middle script, we can add an if statement around addScore,
and check if the collision that just happened was with a GameObject on the bird's layer.
One more bit of future proofing, while we're on the subject. Go back to the Logic Script.
And, let's take the AddScore function, and in these empty brackets we'll write int
scoreToAdd. And then instead of adding one, we'll add scoreToAdd.
Then in the pipe middle script, we can write a 1 in the brackets after addScore. Right
now this does exactly the same thing as we had before. But, as you can surely guess,
you could later add some other goal in the game that adds, say, 5 to your score.
This allows us to make a function more versatile, as it can be used
in different ways, from different places. Part of being a good programmer, I think,
is making stuff less rigid, and keeping it open for future ideas.
This makes it easier and faster to iterate on your designs.
Right! Recap!
UI is just another GameObject, but if we want to reference any
of these components we'll need to add using UnityEngine.UI to the top of the script.
GameObjects can be completely invisible things,
merely there to keep track of rules, logic, score, and so on.
If we want to a reference a component when one of the GameObjects is not in the scene,
we'll need to find that component during run time.
One way to do this is to use tags, findGameObject, and GetComponent.
A public function can be run from another script, as long as you have a reference
to that script. And we can even pass in variables when that function runs.
And Collisions and triggers can be used to make stuff happen when two objects touch.
Speaking of collisions, let's move on to the next step...
The final step is to add a fail state. When the bird hits the pipes, the game is over.
We'll do this by making a game over screen, and have it appear when the bird crashes
into a pipe. The game over screen will have a button, which we can use to reset the game.
First, let's make that game over screen. On the canvas GameObject,
add a new empty one called game over screen. Then, in that parent, add a text for game over.
And also a a button - that's also under legacy. Resize it. And change
the text on the button - the text can be found as a child on the button itself.
So back on the button GameObject, on the button component, you'll see
this bit that says On Click. This is an event, and it allows us to call a
public function on a GameObject. So let's make a function for restarting the level.
We can put this code in the logic script,
underneath our addScore function. You could make a seperate script if you want,
but I think this is fine. Let's make another public function called restartGame,
and in here we'll write code to restart the scene. Just like before with the UI, if we're
managing scenes then we'll need to add a line the top - this time, using UnityEngine.SceneManagment.
Now in our function, we'll call up the SceneManager and then, dot, LoadScene. This
is looking for the name of a scene. Literally the filename. But because we want the current scene we
can simply type SceneManager dot GetActiveScene, brackets, dot name. Close off all the brackets.
Now back in Unity, add an event to this button.
Then drag in the logic GameObject. and find the restartGame function.
Give it a test and... nice. Every time we press the button, the game begins anew.
Now obviously we don't want this to be on the screen all the time - just when we fail. So,
we can just take the whole game over screen GameObject and disable it with this checkmark.
Then we'll make it show up when the bird hits into the pipes.
Let's write the function first. Again in the logic script, let's make a public function for gameOver.
We'll need to make a reference to the game over screen GameObject.
And fill it in Unity.
And then we can simply type gameoverscreen.SetActive true in this function.
So we want this function to trigger when the bird crashes into a pipe.
Back on the bird script, let's reuse that code from before to access the logic script
from the bird script. Yes, we could drag and drop the reference in Unity, but hey,
we've written this code now. And then we're going to do a similar thing to the trigger code, but this time
we'll use OnCollisionEnter2D, because the pipes are solid objects, and not set to be triggers.
And when that collision occurs, trigger the game over script with logic.gameOver.
Back in Unity... it kind of works, but we can still play in the game over screen. Not ideal.
So, I've talked about a few key variable types, already. Floats
and ints are numbers. And string is usually for text. The other important one is a bool,
short for boolean. This is a really simple type that is either true, or false. On,
or off. Yes, or no. It's a great way to simply check or change something's state.
So let's have a bool called birdisalive, and make sure it starts as true.
Then when the collision happens, we'll set birdisalive to false.
And finally, we'll add an extra condition to our very first if statement. We're going to
say if the space bar has just been pressed and... written with two ampersands... and birdisalive is
equal to true. Actually, we don't need to add this equals equals true thing. It does the exact
same thing without it. But, again, it's up to you - maybe it's easier to read this with the full code written out.
Anyway, now, the bird won't flap if it's dead, which seems quite logical to me.
The final thing to do is to build the game. Which is really easy. Pick file, build settings, and build. Pick a folder on
your hard drive. And let Unity do its work. Then you can open this file to play your game! Amazing.
In a very short period of time, we have made a pretty functional game.
And what’s more, we’ve learned loads of fundamental lessons about Unity.
We have made a character that moves in response to our input. We have
spawned in new objects on a timer. We have created a UI that shows a score,
and made that score tick up when conditions are met. And we've got the ability to get a game over,
and start again.
Now, I should note that there are different - and perhaps better ways to do pretty much everything
in this tutorial. For example - I used Unity's old way of checking for inputs, and the company
has since developed a much, much better Input System. But it's a lot more complicated to
use - so this simple method is great for now, and you can look into the new input system later down
the line, when you feel more confident. That's how it went for me. There's also TextMeshPro, which
has replaced the old legacy UI system - so you'll want to graduate to that, at some point, as well.
Anyway, these are lessons that will be useful, for making all sorts of games.
But... the game isn't quite finished yet. There's still a few more things to figure out. Though,
I don't want to tell you how to do everything. So i'm gonna give you some suggestions for how
to finish up the game, but I want you to try and figure it out for yourself.
So first of all, we need to have a game over if the bird goes off the screen.
That shouldn't be too hard. There's also a bug where the score can go up,
even after a game over. Try to solve that one too.
We also want sound effects. I want you to add an Audio Source component to the logic manager.
fill it with a sound effect file. Reference it on the script. And have it play when the score goes up.
Then, i want you to play around with the particle system to make clouds appear in the game.
Next, open the animation window, and add some flapping wings to the bird.
Then i want you to add another scene to make a title screen,
so the game doesn't immediately launch into the action. Here's a clue:
you'll need to add this new scene to the build settings window.
And finally, if you want a real challenge - use PlayerPrefs
to save the player's high score to the hard drive, and draw that on the UI as well.
For each one of these, you will probably want to Google the relevant terms, read the Unity docs,
watch some quick tutorial videos, or ask for help in the comments down below.
Next, you could expand on Flappy Bird. Get creative
and add in ideas or designs that weren't there in the original iPhone game.
For example, with a little messing around I gave the bird the ability to shoot out a missile,
and then I added targets to the pipes. You've now got to hit the target with a missile to open
a gap you can flap through. It's pretty cool, and adds a lot more depth to the simple game.
In fact, I'd love to see how you might expand on the original game. If
you make something interesting, record a bit of footage, pop it on YouTube,
and drop a link in the comments. I might feature some of them in the future.
And then, finally, I'd recommend that you take
another simple game and try to remake it in Unity, like we just did right now.
This is a great technique because you don't have to worry about art or design...
just code. And the problem-solving puzzles you'll face are a perfect example of
what real game development will be like.
Good candidates for this include Pong, Space Invaders, Breakout, Pop the Lock, Angry Birds,
various WarioWare mini games, and that dinosaur game that plays in Chrome if your internet's broken.
So, in this video I wanted to teach you the
fundamental concepts behind Unity - but, the rest is up to you.
Luckily, I reckon this sort of hands-on, self-directed, learn from your mistakes style of
learning is the most fun and effective way to make stuff stick. But we’ll see!
Let me know how you got on in the comments down below. And if you
want to watch my game development story - which is still ongoing,
promise - then click here, for episode one of Developing.
Thanks very much to my Patrons - they're the reason you don't get mid-roll
ads in a looong video like this one. You can help support GMTK at Patreon.com.