Escolar Documentos
Profissional Documentos
Cultura Documentos
Let’s grant him a weapon and some ammo! This will involve a bit more of
scripting, but be confident. It’s worth it.
Projectile
First, before allowing the player to shoot, we need to define a game
object that represents the projectiles he will use.
A trigger collider raises an event when colliding but is not used by the
physics simulation.
It means that a shot will pass through an object on touching — there
won’t be any “real” interaction at all. Yet, the other collider is going to
have its “OnTriggerEnter2D” event raised.
using UnityEngine;
/// <summary>
/// Projectile behavior
/// </summary>
public class ShotScript : MonoBehaviour
{
// 1 - Designer variables
/// <summary>
/// Damage inflicted
/// </summary>
public int damage = 1;
/// <summary>
/// Projectile damage player or enemies?
/// </summary>
public bool isEnemyShot = false;
void Start()
{
// 2 - Limited time to live to avoid any leak
Destroy(gameObject, 20); // 20sec
}
}
Then drag the shot game object in the “Project” pane to create
a Prefab from it. We will need it in a few steps.
using UnityEngine;
/// <summary>
/// Handle hitpoints and damages
/// </summary>
public class HealthScript : MonoBehaviour
{
/// <summary>
/// Total hitpoints
/// </summary>
public int hp = 1;
/// <summary>
/// Enemy or player?
/// </summary>
public bool isEnemy = true;
/// <summary>
/// Inflicts damage and check if the object should be destroyed
/// </summary>
/// <param name="damageCount"></param>
public void Damage(int damageCount)
{
hp -= damageCount;
if (hp <= 0)
{
// Dead!
Destroy(gameObject);
}
}
If you have worked on a game object instance instead of the Prefab, don’t
be scared: you can click on the “Apply” button at the top of the
“Inspector” to add the changes to the Prefab.
Make sure the shot and the Poulpi are on the same line to test the
collision.
Note: the 2D physics engine, Box2D, doesn’t use the Z position. Colliders
2D will always be in the same plane even if your game objects are not.
Firing
Delete the shot in the scene. It has nothing to do there now that we have
finished it.
This script will be reused everywhere (players, enemies, etc.). Its purpose
is to instantiate a projectile in front of the game object it is attached to.
Here’s the full code, bigger than usual. The explanations are below:
using UnityEngine;
/// <summary>
/// Launch projectile
/// </summary>
public class WeaponScript : MonoBehaviour
{
//--------------------------------
// 1 - Designer variables
//--------------------------------
/// <summary>
/// Projectile prefab for shooting
/// </summary>
public Transform shotPrefab;
/// <summary>
/// Cooldown in seconds between two shots
/// </summary>
public float shootingRate = 0.25f;
//--------------------------------
// 2 - Cooldown
//--------------------------------
void Start()
{
shootCooldown = 0f;
}
void Update()
{
if (shootCooldown > 0)
{
shootCooldown -= Time.deltaTime;
}
}
//--------------------------------
// 3 - Shooting from another script
//--------------------------------
/// <summary>
/// Create a new projectile if possible
/// </summary>
public void Attack(bool isEnemy)
{
if (CanAttack)
{
shootCooldown = shootingRate;
// Assign position
shotTransform.position = transform.position;
/// <summary>
/// Is the weapon ready to create a new projectile?
/// </summary>
public bool CanAttack
{
get
{
return shootCooldown <= 0f;
}
}
}
The first one is needed to set the shot that will be used with this weapon.
The shootingRate variable has a default value set in the code. We will not
change it for the moment. But you can start the game and experiment
with it to test what it does.
2. Cooldown
Guns have a firing rate. If not, you would be able to create tons of
projectiles at each frame.
void Update()
{
// ...
// 5 - Shooting
bool shoot = Input.GetButtonDown("Fire1");
shoot |= Input.GetButtonDown("Fire2");
// Careful: For Mac users, ctrl + arrow is a bad idea
if (shoot)
{
WeaponScript weapon = GetComponent<WeaponScript>();
if (weapon != null)
{
// false because the player is not an enemy
weapon.Attack(false);
}
}
// ...
}
It doesn’t matter at this point if you put it after or before the movement.
What did we do ?
Button down: you can notice that we use the GetButtonDown() method to
get an input. The “Down” at the end allows us to get the input when the
button has been pressed once and only once. GetButton() returns true at each
frame until the button is released. In our case, we clearly want the
behavior of the GetButtonDown() method.
Launch the game with the “Play” button. You should get this:
The bullets are too slow? Experiment with the “Shot” prefab to find a
configuration you’d like.
Bonus: Just for fun, add a rotation to the player, like (0, 0, 45). The shots
have a 45 degrees movement, even if the rotation of the shot sprite is not
correct as we didn’t change it too.
Next step
We have a shooter! A very basic one, but a shooter despite everything.
You learned how to create a weapon that can fire shots and destroy other
objects.
But this part is not over! We want enemies that can shoot too. Take a
break, what comes next is mainly reusing what we did here.
If you like to do it the hard way, you could also recreate a whole new
sprite, rigibody, collider with trigger, etc.
/// <summary>
/// Enemy generic behavior
/// </summary>
public class EnemyScript : MonoBehaviour
{
private WeaponScript weapon;
void Awake()
{
// Retrieve the weapon only once
weapon = GetComponent<WeaponScript>();
}
void Update()
{
// Auto-fire
if (weapon != null && weapon.CanAttack)
{
weapon.Attack(true);
}
}
}
We need to:
/// <summary>
/// Enemy generic behavior
/// </summary>
public class EnemyScript : MonoBehaviour
{
private WeaponScript[] weapons;
void Awake()
{
// Retrieve the weapon only once
weapons = GetComponentsInChildren<WeaponScript>();
}
void Update()
{
foreach (WeaponScript weapon in weapons)
{
// Auto-fire
if (weapon != null && weapon.CanAttack)
{
weapon.Attack(true);
}
}
}
}
A possible result:
Player-enemy collision
Let’s see how we can handle the collision between the
player and an enemy, as it is quite frustrating to see
them block each other without consequences…
damagePlayer = true;
}
Parallax scrolling
For the moment, we have created a static scene with a player and some
enemies. It’s a bit boring. Time to enhance our background and scene.
An effect that you find in every single 2D game for 15 years is “parallax
scrolling”.
1. First choice: The player and the camera move. The rest is fixed.
2. Second choice: The player and the camera are static. The level is a
treadmill.
In order to add the parallax scrolling effect to our game, the solution is to
mix both choices. We will have two scrollings:
Note: you may ask: “Why don’t we just set the camera as a child of the
player object?”. Indeed, in Unity, if you set an object (camera or not) as a
sub-child of a game object, this object will maintain its relative position to
its parent. So if the camera is a child of the player and is centered on
him, it will stay that way and will follow him exactly. It could be a
solution, but this would not fit with our gameplay.
Here is what we will do: We position the Poulpies on the scene directly
(by dragging the Prefab onto the scene). By default, they are static and
invincibles until the camera reaches and activates them.
The nice idea here is that you can use the Unity editor to set the enemies.
You read right: without doing anything, you already have a level editor.
Planes
First, we must define what our planes are and for each,
if it’s a loop or not. A looping background will repeat
over and over again during the level execution. E.g.,
it’s particularly useful for things like the sky.
Layer Loop
Background with the sky Yes
Background (1st row of flying platforms) No
Middleground (2nd row of flying platforms) No
Foreground with players and enemies No
We could add as many layers of background objects as
we want.
Careful with that axe, Eugene: if you add layers ahead of the
foreground layer, be careful with the visibility. Many games do not use
this technique because it reduces the clearness of the game, especially in
a shmup where the gameplay elements need to be clearly visible.
Simple scrolling
We will start with the easy part: scrolling
backgrounds without looping.
/// <summary>
/// Moving direction
/// </summary>
public Vector2 direction = new Vector2(-1, 0);
/// <summary>
/// Movement should be applied to camera
/// </summary>
public bool isLinkedToCamera = false;
void Update()
{
// Movement
Vector3 movement = new Vector3(
speed.x * direction.x,
speed.y * direction.y,
0);
movement *= Time.deltaTime;
transform.Translate(movement);
The result:
Not bad! But we can see that enemies move and shoot
when they are out of the camera, even before they
spawn!
In our case, the idea is that we will get all the children
on the layer and check their renderer.
A note about using the renderer component: This method won’t work
with invisible objects (e.g., the ones handling scripts). However, a use
case when you need to do this on invisible objects is unlikely.
Create a static method starting with a first parameter which looks like
this: this Type currentInstance. The Type class will now have a new method
available everywhere your own class is available.
Inside the extension method, you can refer to the current instance calling
the method by using the currentInstance parameter instead of this.
In this tutorial, we are not using namespaces at all. However, in your real
project, you might consider to use them. If not, prefix your classes and
behaviors to avoid a collision with a third-party library (like NGUI).
The real reason behind not using namespaces was that during the Unity 4
days (this tutorial was originally written for Unity 4.3), a namespace
would prevent the use of default parameters. It’s not a problem anymore,
so: use namespace!
Full “ScrollingScript”
Observe the full “ScrollingScript” (explanations below):
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
/// <summary>
/// Parallax scrolling script that should be assigned to a layer
/// </summary>
public class ScrollingScript : MonoBehaviour
{
/// <summary>
/// Scrolling speed
/// </summary>
public Vector2 speed = new Vector2(10, 10);
/// <summary>
/// Moving direction
/// </summary>
public Vector2 direction = new Vector2(-1, 0);
/// <summary>
/// Movement should be applied to camera
/// </summary>
public bool isLinkedToCamera = false;
/// <summary>
/// 1 - Background is infinite
/// </summary>
public bool isLooping = false;
/// <summary>
/// 2 - List of children with a renderer.
/// </summary>
private List<SpriteRenderer> backgroundPart;
// Sort by position.
// Note: Get the children from left to right.
// We would need to add a few conditions to handle
// all the possible scrolling directions.
backgroundPart = backgroundPart.OrderBy(
t => t.transform.position.x
).ToList();
}
}
void Update()
{
// Movement
Vector3 movement = new Vector3(
speed.x * direction.x,
speed.y * direction.y,
0);
movement *= Time.deltaTime;
transform.Translate(movement);
// 4 - Loop
if (isLooping)
{
// Get the first object.
// The list is ordered from left (x position) to right.
SpriteRenderer firstChild = backgroundPart.FirstOrDefault();
if (firstChild != null)
{
// Check if the child is already (partly) before the camera.
// We test the position first because the IsVisibleFrom
// method is a bit heavier to execute.
if (firstChild.transform.position.x < Camera.main.transform.position.x)
{
// If the child is already on the left of the camera,
// we test if it's completely outside and needs to be
// recycled.
if (firstChild.IsVisibleFrom(Camera.main) == false)
{
// Get the last child position.
SpriteRenderer lastChild = backgroundPart.LastOrDefault();
Explanations
The problem is that these methods are also called when rendered by the
“Scene” view of the Unity editor. This means that we will not get the
same behavior in the Unity editor and in a build (whatever the platform
is). This is dangerous and absurd. We highly recommend to avoid these
methods.
/// <summary>
/// Enemy generic behavior
/// </summary>
public class EnemyScript : MonoBehaviour
{
private bool hasSpawn;
private MoveScript moveScript;
private WeaponScript[] weapons;
private Collider2D coliderComponent;
private SpriteRenderer rendererComponent;
void Awake()
{
// Retrieve the weapon only once
weapons = GetComponentsInChildren<WeaponScript>();
coliderComponent = GetComponent<Collider2D>();
rendererComponent = GetComponent<SpriteRenderer>();
}
// 1 - Disable everything
void Start()
{
hasSpawn = false;
// Disable everything
// -- collider
coliderComponent.enabled = false;
// -- Moving
moveScript.enabled = false;
// -- Shooting
foreach (WeaponScript weapon in weapons)
{
weapon.enabled = false;
}
}
void Update()
{
// 2 - Check if the enemy has spawned.
if (hasSpawn == false)
{
if (rendererComponent.IsVisibleFrom(Camera.main))
{
Spawn();
}
}
else
{
// Auto-fire
foreach (WeaponScript weapon in weapons)
{
if (weapon != null && weapon.enabled && weapon.CanAttack)
{
weapon.Attack(true);
}
}
// 3 - Activate itself.
private void Spawn()
{
hasSpawn = true;
// Enable everything
// -- Collider
coliderComponent.enabled = true;
// -- Moving
moveScript.enabled = true;
// -- Shooting
foreach (WeaponScript weapon in weapons)
{
weapon.enabled = true;
}
}
}
Why not after all? The only thing that is moving in this
layer is him, and the script is not specific to a kind of
object.