SUPERPOWERS TUTORIAL #4
SUPER ASTEROIDS and SUPER SPACEWAR, Chapter 7
Writing Ship and Missile behavior
Initialize ship whit Game.start
Before to write the ship behavior, we need to write when the ship appear in the game (spawn). We do that in the Game.start function of the global script.
We check first what game have been chosen from Menu in nameIndex variable and set the game accordingly.
[...]
namespace Game{
[...]
export function start(){
[...]
// If the game is Asteroids, load the Ship 1 only
if (nameIndex === 0) {
// Set ship 1 in game
setShip(Ships.index.ship1);
// Set visible false the HUD display for ship 2
Sup.getActor("HUD").getChild("UIShip2").setVisible(false);
// Set visible true the HUD display for alien
Sup.getActor("HUD").getChild("UIAlien").setVisible(true);
// Set Timer HUD visible false
Sup.getActor("HUD").getChild("Timer").setVisible(true);
}
// If the game is Spacewar, load two ships
if (nameIndex === 1) {
// Set ship 1 in game
setShip(Ships.index.ship1);
// Set ship 2 in game
setShip(Ships.index.ship2);
// Set visible true the HUD display for ship 2
Sup.getActor("HUD").getChild("UIShip2").setVisible(true);
// Set visible false the HUD display for alien
Sup.getActor("HUD").getChild("UIAlien").setVisible(false);
// Set Timer HUD visible false
Sup.getActor("HUD").getChild("Timer").setVisible(false);
}
[...]
It won't work yet because we need the setShip local function which is used to load the Ship actor in the game.
the setShip function
We write a new function setShip which is not exported because we will use it only from the local module Game.
[...]
namespace Game{
function setShip(shipIndex:number){
// Initialize a new variable of type Sup.Actor
let Ship: Sup.Actor;
// Add the ship 1 or 2 to game scene depending the index
if (shipIndex === 0) {
// Create Ship and set the ship variable with the Ship1 actor
Ship = Sup.appendScene("Ship/0/Prefab")[0];
}
else {
// Create Ship and set the ship variable with the Ship2 actor
Ship = Sup.appendScene("Ship/1/Prefab")[0];
}
// Set behavior variables
Ship.getBehavior(ShipBehavior).index = shipIndex;
// Set spawn position of the ship accordingly to the game an ship index
// If game is Asteroids, set position to center
if (nameIndex === 0) {
Ship.setLocalPosition(Ships.spawns[0]);
}
// If game is Spacewar and Ship is 1, set position to corner down left
else if (shipIndex === 0) {
Ship.setLocalPosition(Ships.spawns[1]);
}
// Else it is Ship 2, set position to corner up right
else {
Ship.setLocalPosition(Ships.spawns[2]);
}
}
}
[...]
To test this code we also need to add index:number; in the ShipBehavior class of the Ship script.
We don't have menu yet to choose game, we still can do test by changing temporarily the nameIndex variable.
If we want to try the Asteroids game :
[...]
export let nameIndex: number = 0; // Temporary allocation
[...]
If we want to try the Spacewar game :
[...]
export let nameIndex: number = 0; // Temporary allocation
[...]
the shipBehavior class
ship datas
We open the Ship script and first initialize the ship local datas we will use in our code.
[...]
class ShipBehavior extends Sup.Behavior {
// Ship index, 0 is ship 1, 1 is ship 2
index: number;
// Ship radius collision
amplitude: number;
// Ship current life
lifes: number;
// Ship status
alive: boolean;
// Ship current score
score: number;
// Spawn position
spawnPosition: Sup.Math.Vector2;
// Current position
position: Sup.Math.Vector2;
// Current movement speed
linearVelocity = new Sup.Math.Vector2();
// Current rotation speed
angularVelocity: number;
// Angle position
angle: number;
// Timer before shooting
shootCooldown: number;
// Timer before respawn
spawnCooldown: number;
// Timer before vulnerability
invincibilityCooldown: number;
// Timer before gameover
gameOverCooldown: number;
awake() {
}
update() {
}
}
Sup.registerBehavior(ShipBehavior);
[...]
ship awakening
When the ship awake, we need to set some variables.
[...]
awake() {
// Starting life to 3
this.lifes = Ships.startLife;
// Set true to alive status
this.alive = true;
// Starting score to 0
this.score = Ships.startScore;
// Starting speed movement on x and y axis to 0
this.linearVelocity.set(0, 0);
// Starting speed rotation to 0
this.angularVelocity = 0;
}
[...]
ship starting
Then when the actor is loaded, we can get its parameters and set the local variables of the behavior.
[...]
start() {
// Set the ship default size to half size
this.actor.setLocalScale(Ships.size);
// Set the ship default amplitude related to size
this.amplitude = Ships.amplitude;
// Get the starting position to become the spawnPosition of this behavior
this.spawnPosition = this.actor.getLocalPosition().toVector2();
// Get the starting position to become the current position of this behavior
this.position = this.actor.getLocalPosition().toVector2();
// Get the starting angle to become the current angle of this behavior
this.angle = this.actor.getLocalEulerZ();
}
[...]
ship death
We define a method for the death of the player ship.
Note : Sup.setTimeout is used to have a delay before to jump to the gameOver menu screen.
[...]
die() {
// Decrease life of one
this.lifes--;
// Set false to alive status
this.alive = false;
// Flag to check and update the life HUD
Game.checkLifeHUD = true;
// If life is 0, then the game is over
if (this.lifes === 0) {
// If this is the Asteroids game, death of ship mean victory for Alien
if(Game.nameIndex === 0) {
Sup.setTimeout(1000, function () { Game.gameOver("alien") });
}
else {
// If this is Spacewar game and death of ship 1 mean victory for ship 2
if (this.index === 0){
Sup.setTimeout(1000, function () { Game.gameOver("ship2") });
}
// Else, this is ship 2 and it is a victory for ship 1
else {
Sup.setTimeout(1000, function () { Game.gameOver("ship1") });
}
}
}
// Check which ship index to see which lose points
if (this.index === 0) {
Game.addPoints(true, Game.points.death);
}
else {
Game.addPoints(false, Game.points.death);
}
// Flag to check and update the score HUD
Game.checkScoreHUD = true;
// Set timer before respawn
this.spawnCooldown = Ships.respawnTimer;
// Set ship model visibility to false
this.actor.getChild('Model').setVisible(false);
// Set ship boosts visibility to false
this.actor.getChild('Boost').setVisible(false);
// Set sprite animation explosition to play once
this.actor.getChild('Destruction').spriteRenderer.setAnimation("explode", false);
// Reset speed movement on x and y axis to 0
this.linearVelocity.set(0, 0);
// Reset angular movement to 0
this.angularVelocity = 0;
// Reset angle to default for ship 1 or ship 2
if (this.index === 0){
this.angle = 1.6;
}
else{
this.angle = -1.6;
}
}
[...]
We don't forget to comment the Game.addPoints calls to do tests later without bug.
ship respawn
[...]
spawn() {
// The ship respawn to spawn position
this.position = this.spawnPosition.clone();
// Set the new current position to the Ship actor
this.actor.setLocalPosition(this.position);
// Set the new angle to the Ship actor
this.actor.setLocalEulerZ(this.angle);
// The ship model visibility to true
this.actor.getChild('Model').setVisible(true);
// Set timer for invincibility
this.invincibilityCooldown = Ships.invincibleTimer;
}
[...]
ship shooting
[...]
shoot() {
// Initialize a new missile
let missile: Sup.Actor;
// If the ship is ship 1 then create a Ship 1 missile and set the variable missile to the Missile actor
if (this.index === 0) {
missile = Sup.appendScene("Ship/0/Missile/Prefab")[0];
}
// Else do the same but for the missile of ship 2
else {
missile = Sup.appendScene("Ship/1/Missile/Prefab")[0];
}
// Set position of the actor to the current position of the ship
missile.setLocalPosition(this.position);
// Set local variables of the missile behavior
// Report the position of the ship to the variable position of the behavior
missile.getBehavior(ShipMissileBehavior).position = this.position.clone();
// Report the angle of the ship to the angle direction of the missile
missile.getBehavior(ShipMissileBehavior).angle = this.angle;
// Set the shipIndex related to this missile
missile.getBehavior(ShipMissileBehavior).shipIndex = this.index;
// Set Shooting timer to be able to shoot again
this.shootCooldown = Ships.shootingTimer;
}
[...]
To make it work, we will need to set the local variable of the Missile behavior in Ship/Missile script.
class ShipMissileBehavior extends Sup.Behavior {
shipIndex: number;
position: Sup.Math.Vector2;
angle: number;
[...]
ship boost
We add a little sprite game when the ship is moving or rotating with this two methods.
[...]
boost(intensity: string) {
// Create a new variable boost that get the Boost actor child of Ship actor
let boost:Sup.Actor = this.actor.getChild("Boost");
// Set the boost actor visible true
boost.setVisible(true);
// Set animation to both sprite with the intensity normal or fast
boost.getChild("0").spriteRenderer.setAnimation(intensity);
boost.getChild("1").spriteRenderer.setAnimation(intensity);
}
rotateBoost(direction: string) {
// Create a new variable boost that get the Boost actor child of Ship actor
let boost:Sup.Actor = this.actor.getChild("Boost");
// Set the boost actor visible true
boost.setVisible(true);
// If rotate on the left direction
if(direction === "left"){
// Switch animation for right boost stronger
boost.getChild("0").spriteRenderer.setAnimation("fast");
boost.getChild("1").spriteRenderer.setAnimation("normal");
}
// If rotate on the right direction
if(direction === "right"){
// Switch animation for left boost stronger
boost.getChild("1").spriteRenderer.setAnimation("fast");
boost.getChild("0").spriteRenderer.setAnimation("normal");
}
}
[...]
ship process
Now we have all our methods ready, we can write the whole process of the player ship.
- Keep respawning, will keep looping and decrease spawn timer
- Keep shooting, call the shoot method when shoot key pressed
- Keep moving, move the ship when key pressed
- Keep rotating, rotate the ship when key pressed
- Keep slowing down, slow down the acceleration of linear and angular velocities
- Stay on the game screen, same as asteroids and alien ship
- Update position and angle, report behavior change to the Actor
- Blinking, to display ship invincibility, it is placed after the moving, rotating blocks of code but will return before the collisions because as long at it will blink, the ship won't be able to take damages but is still maneuverable.
[...]
update() {
// Keep respawning
// If the spawnCooldown timer is more than 0
if (this.spawnCooldown > 0) {
// Decrease by one the timer
this.spawnCooldown--
// If the spawnCooldown is 0
if (this.spawnCooldown === 0) {
// Call the spawn method
this.spawn();
}
//restart update loop to skip the following code
return;
}
// Keep shooting
// If the shootCooldown timer is more than 0
if (this.shootCooldown > 0) {
// Decrease by one the timer
this.shootCooldown--;
}
// If the timer is 0 (!0 = true)
if (!this.shootCooldown) {
// If the shoot key is pressed
if(Sup.Input.wasKeyJustPressed(Ships.commands[this.index].shoot)){
// Call the shoot method
this.shoot();
}
}
// Keep moving
// If forward key is pressed down
if (Sup.Input.isKeyDown(Ships.commands[this.index].forward)){
// Set the impulse with the linearAcceleration
let impulse = new Sup.Math.Vector2(Ships.linearAcceleration, 0);
// Convert the impulse to the current angle
impulse.rotate(this.angle);
// Add the impulse to the linearVelocity
this.linearVelocity.add(impulse);
// Call the boost method with a normal intensity
this.boost("normal");
// If the boost key is pressed down
if (Sup.Input.isKeyDown(Ships.commands[this.index].boost)) {
// Add a second time the impulse to the linearVelocity
this.linearVelocity.add(impulse);
// Call the boost method with a fast intensity
this.boost("fast");
}
}
else {
// Set visible false booster if not going forward
this.actor.getChild("Boost").setVisible(false);
}
// Keep rotating
// If left key is pressed down
if (Sup.Input.isKeyDown(Ships.commands[this.index].left)){
// The angularVelocity get the angularAcceleration
this.angularVelocity += Ships.angularAcceleration;
// Boost sprite for left side
this.rotateBoost("left");
}
// If right key is pressed down
if (Sup.Input.isKeyDown(Ships.commands[this.index].right)){
// The angularVelocity get the opposite angularAcceleration
this.angularVelocity -= Ships.angularAcceleration;
// Boost sprite for left side
this.rotateBoost("right");
}
// Set boost to default if key left, right and forward are NOT pressed
if (!Sup.Input.isKeyDown(Ships.commands[this.index].left) &&
!Sup.Input.isKeyDown(Ships.commands[this.index].right) &&
!Sup.Input.isKeyDown(Ships.commands[this.index].forward)){
this.actor.getChild("Boost").setVisible(false);
this.actor.getChild("Boost").getChild("0").setVisible(true);
this.actor.getChild("Boost").getChild("1").setVisible(true);
}
// Keep slowing down
// The linearVelocity multiply the linearDamping
this.linearVelocity.multiplyScalar(Ships.linearDamping);
// The angularVelocity multiply the angularDamping
this.angularVelocity *= Ships.angularDamping;
// Stay on the game screen
if (this.position.x > Game.bounds.width / 2) {
this.position.x = -Game.bounds.width / 2;
}
if (this.position.x < -Game.bounds.width / 2) {
this.position.x = Game.bounds.width / 2;
}
if (this.position.y > Game.bounds.height / 2) {
this.position.y = -Game.bounds.height / 2;
}
if (this.position.y < -Game.bounds.height / 2) {
this.position.y = Game.bounds.height / 2;
}
// Update position and angle
// Add the linearVelocity to the current position
this.position.add(this.linearVelocity);
// Set the new current position to the Ship actor
this.actor.setLocalPosition(this.position);
// Add the angularVelocity to the current angle
this.angle += this.angularVelocity;
// Set the new angle to the Ship actor
this.actor.setLocalEulerZ(this.angle);
// Blinking
// If the invincibilityCooldown Timer is more than 0
if (this.invincibilityCooldown > 0) {
// Decrease by one the timer
this.invincibilityCooldown--;
// Set actor visible become true every half second, false the other half and stay visible at the end
this.actor.setVisible(this.invincibilityCooldown % 60 < 30);
// When invincibilityCooldown reach 1, get back vulnerability
if (this.invincibilityCooldown === 1) {
// Set true to alive status
this.alive = true;
}
// Restart update loop to skip the collision code
return;
}
// Collision code will come here, we remove this block when ch8 start
if (Sup.Input.wasKeyJustPressed("X")){
this.die(); // Temporary debug test for ship death, press keyboard X to kill the ship
}
}
[...]
We now have our player ship under control, moving, rotating, slowing down, shooting (but the missile don't have behavior).
We need now to give some behavior to the missiles.
Ship missiles
datas
class ShipMissileBehavior extends Sup.Behavior {
// shipIndex owner of this missile actor
shipIndex: number;
// Current position
position: Sup.Math.Vector2;
// Current movement velocity
velocity: Sup.Math.Vector2;
// Missile trajectory angle
angle: number;
// Timer before death
lifeTime: number;
[...]
}
Sup.registerBehavior(ShipMissileBehavior);
start and onDestroy methods
When a missile born we add it to the global list of missiles that we will use to check collisions, when the missile die we remove it from the list.
[...]
start() {
// this.actor.setLocalPosition(this.position);
this.velocity = new Sup.Math.Vector2(Ships.missileSpeed, 0);
this.velocity.rotate(this.angle);
this.lifeTime = Ships.missileLife;
// Sup.log("Missile created.")
Ships.missiles[this.shipIndex].push(this.actor);
// Sup.log(Ships.missiles);
}
[...]
onDestroy(){
// Remove the current actor from the Global list from the shipIndex owner
Ships.missiles[this.shipIndex].splice(Ships.missiles[this.shipIndex].indexOf(this.actor), 1);
}
[...]
missile process
Missile behavior is very simple, the missile born, move in one direction for a certain amount of time and then die.
[...]
update() {
// Keep moving
this.position.add(this.velocity);
this.actor.setLocalPosition(this.position);
// If the timer is superior to 0, decrease by one
if (this.lifeTime > 0) {
this.lifeTime--;
// If the timer reach 10 frame before death, play the explode animation once
if (this.lifeTime === 10) {
this.actor.getChild("Sprite").spriteRenderer.setAnimation("explode", false);
}
}
// Once the lifeTime timer reach 0, destroy the Actor
else {
this.actor.destroy();
}
}
[...]
Before to be able to execute this code we need to create a new missile list when the game Start in the module Game of the Global script.
[...]
export function start(){
[...]
// Set new Ships.missiles list
Ships.missiles = [[], []];
[...]
Alien missiles
datas
class AlienMissileBehavior extends Sup.Behavior {
// Current position
position: Sup.Math.Vector2;
// Current velocity
velocity: Sup.Math.Vector2;
// Target position
target: Sup.Math.Vector2;
[...]
}
Sup.registerBehavior(AlienMissileBehavior);
start and onDestroy methods
Like for the player ship missile, as missile born we add it to the global list of missiles that we will use to check collisions, when the missile die we remove it from the list.
[...]
start() {
// Set current position from the actor position
this.position = this.actor.getLocalPosition().toVector2();
// Get player ship position to define as target for this missile
this.target = Sup.getActor("Ship1").getLocalPosition().toVector2();
// Get angle trajectory between this actor position and the target position
this.angle = this.position.angleTo(this.target);
// Create velocity with the Alien.missileSpeed value
this.velocity = new Sup.Math.Vector2(Alien.missileSpeed, 0);
// Convert velocity with the angle trajectory
this.velocity.rotate(this.angle);
// Add the current actor the Alien.missiles list
Alien.missiles.push(this.actor);
}
[...]
onDestroy() {
// Remove this actor from the Alien.missiles list
Alien.missiles.splice(Alien.missiles.indexOf(this.actor), 1);
}
[...]
missile process
The Alien ship missile is a bit different of the Player ship missile for using a target position (the player ship) to define the angle of trajectory. The missile reach this target position and explode.
[...]
update() {
// While the missile has no reached the target position, keep moving
// Add current velocity to current position
this.position.add(this.velocity);
// Update current position to missile actor
this.actor.setLocalPosition(this.position);
// Get the distance between current position and target position
let distance = this.target.distanceTo(this.position);
// When the distance to target is nearly reached, the missile explode
if (distance < 0.5) {
this.actor.getChild("Sprite").spriteRenderer.setAnimation("explode", false);
// When the distance is close to 0, the missile actor is destroyed
if (distance < 0.1) {
this.actor.destroy();
}
}
}
[...]
Before to be able to execute this code we need to create a new missile list when the game Start in the module Game of the Global script.
[...]
export function start(){
[...]
// Set new Alien.missiles list
Alien.missiles = [];
[...]
Good, we have now everything we need in place to start an important game mechanic, the collision system, we will see that in the next chapter.