TGB/BasicTutorial4

From TDN

This page is a Work In Progress.


Contents

Overview

The following links can take you to previous/further sections of the tutorial:

TGB Shooter Tutorial Part 1 - Importing Images
TGB Shooter Tutorial Part 2 - Level and Player Setup
TGB Shooter Tutorial Part 3 - Know Your Enemy
TGB Shooter Tutorial Part 4 - Picking a Fight
TGB Shooter Tutorial Part 5 - Adding Particle Effects

For Part 4 - Picking a Fight, please continue reading below.


TGB Basic Tutorial - Part 4


Making the Player Shoot


First we will need to get the player shooting at the enemy. To do this requires quite a bit of scripting. First, in player.cs let's add the move map binding to the space bar to make the player shoot, and also set the speed of the missiles:


function PlayerShip::onLevelLoaded(%this, %scenegraph)
{
   $pShip = %this;
   $pShip.hSpeed = 100;
   $pShip.vSpeed = 80;
   $pShip.missileSpeed = 500;
   
   moveMap.bindCmd(keyboard, "up", "pShipUp();", "pShipUpStop();");
   moveMap.bindCmd(keyboard, "down", "pShipDown();", "pShipDownStop();");
   moveMap.bindCmd(keyboard, "left", "pShipLeft();", "pShipLeftStop();");
   moveMap.bindCmd(keyboard, "right", "pShipRight();", "pShipRightStop();");
   moveMap.bindCmd(keyboard, "space", "$pShip.fireMissile();", "");
}


Now that we have the space bar calling the fireMissile() function, let's create that function. Place this code at the bottom of your player.cs file:


function PlayerShip::fireMissile(%this)
{
   %this.playerMissile = new t2dStaticSprite()
   {
      scenegraph = %this.scenegraph;
      class = PlayerMissile;
      missileSpeed = %this.missileSpeed;
      player = %this;
   };
   
   %this.playerMissile.fire();
}


This function creates a new t2dStaticSprite object (the same kind of objects as the player and enemy ships), and sets its class and speed. By setting the scenegraph field, it attaches the sprite to the current scenegraph which will draw the sprite on the screen. The player field is set so it can be referenced later on. After creating the missile object, it calls the fire function of the PlayerMissile to actually fire the missile.


In your yourProjectName/game/gameScripts folder, create a new file named playerMissile.cs. This file will hold everything that we need for our player's missile to work properly. The first function we will need in our playerMissile.cs file is our fire function:


function PlayerMissile::fire(%this)
{
   // Pick the image and set its size
   %this.setImageMap(playerMissileImageMap);
   %this.setSize(6.25, 3.125);
   
   // Set the missile position to where the player is, plus
   // a small offset so that the missile lines up with missile
   // launcher in the player ship image
   %this.setPositionX(%this.player.getPositionX() + 1);
   %this.setPositionY(%this.player.getPositionY() + 1);
   %this.setLinearVelocityX(%this.missileSpeed);
   
   // Use the camera view bounds as the basis for the missile's world limits
   %cameraViewBounds = sceneWindow2D.getCurrentCameraArea();
   %this.setWorldLimit( kill, getword(%cameraViewBounds, 0) - (%this.getWidth()  / 2),
                              getword(%cameraViewBounds, 1) - (%this.getHeight() / 2),
                              getword(%cameraViewBounds, 2) + (%this.getWidth()  / 2),
                              getword(%cameraViewBounds, 3) + (%this.getHeight() / 2) );
   
   // Turn on collision, but turn off physics
   %this.setCollisionActive( true, true );
   %this.setCollisionPhysics(false, false);
   %this.setCollisionCallback(true);
}


This function handles most of the settings of our missile. It sets the world limit to an area slightly larger than the camera view, and uses the KILL limit mode which will remove from the screen and delete the missile when it hits any of the world limits. It also sets the missile's appearance, size, speed, and position. The last three lines turn the collision on, turn the physics off, and turn on collision callbacks.


The next function we need deals with what is going to happen to our missile when it collides with something:


function PlayerMissile::onCollision( %srcObj, %dstObj, %srcRef, %dstRef, %time, %normal, %contactCount, %contacts )
{
    if (%dstObj.class $= "EnemyShip") {
       %srcObj.explode();	
       %dstObj.explode();
    }
}


This function is pretty simple, it is only triggered when the missile collides with something. When it collides, it checks the class of what it collided with. If it collided with an object of the EnemyShip class, it calls both the explode() function of the missile and the explode() function of the enemy ship instance, causing both the missile and ship to explode.


The final function we will need for the PlayerMissile class is the explode() function that is triggered when it collides with an enemy ship:


function PlayerMissile::explode(%this)
{
    %this.safeDelete();
}


All this function does is delete the missile when it is called. Now that we have our playerMissile set up, we need to add the explode function to our enemy.


Open up the enemy.cs file. After you open it, add this function to the file:


function EnemyShip::explode(%this)
{
   %this.spawn();
}


All it does is call the enemy's spawn function when it is triggered. Save your files (if you haven't already) and go into your main.cs file and add the line to load the playerMissile.cs file:


   // Exec game scripts
   exec("./gameScripts/game.cs");
   exec("./gameScripts/player.cs");
   exec("./gameScripts/enemy.cs");
   exec("./gameScripts/playerMissile.cs"); // Add this line


That is all of the code you need to get your enemy to die.


Collision Polygons


If you run the game right now, you may notice that the enemy ships will disappear when the missile seems to get really close but doesn't actually touch the enemy ship. The reason is that the missile is colliding with the ship's sprite, which is a simple rectangular shape. In other words, TGB does not look at the image data for sprites and do pixel-level collision detection; that'd be far too slow. Instead, TGB uses convex polygon collision detection, and by default the polygon for every sprite is a simple rectangle.


To change the polygon to more accurately fit the ship's image, open the level builder, select one of your enemy ships, and click on the Edit Collision Polygon button above the ship.


Image:TGB_Tutorials_Shooter_EnemyShipCollisionPolygon1.jpg


The window that opens is called the Collision Polygon Editor. Here you outline the ship's image by simply clicking and dragging points around the ship's image. If any of the points turns red, it means the point's location is invalid. The reason the point would be invalid is that the polygon that the points form would not be considered a "convex" polygon. A simple test to understand what a convex polygon is: Imagine that a line segment connecting two points of the polygon is made into an infinite line. That line can not cross into the interior of the polygon. This limitation does make the collision polygons imperfect, but for the vast majority of cases they'll work perfectly fine. Below is the final set of polygon points created for the enemy ship. Your points may vary, and that's perfectly fine. It doesn't have to be exact.


Image:TGB_Tutorials_Shooter_EnemyShipCollisionPolygon2.jpg


Once you have your defined the collision polygon for the enemy ship, you need to turn on collision for the ship in the Collision pane under the Edit tab:


Image:TGB_Tutorials_Shooter_ShipCollisionSettings.jpg


Next, delete all of the other copies of the enemy ship that you previously created, since those copies now do not have the collision polygon and settings defined properly. After you delete them, you can duplicate the enemy ship that does have the collision polygon defined as many times as you wish.


At this point, go ahead and run the game once more and you should be able to shoot the ships out of the sky and see that the missile/ship collisions are much more accurate than before.



Making Enemies Shoot Back


After a few minutes of blasting helpless enemy ships with your missiles, you may get bored with what's going on. After all, you are invincible, there is nothing that can hurt you... yet. This section deals with adding collision to the player and giving the enemies weapons to blow up the player's ship.


First, give the player a collision polygon the same way you did with the enemy ship:


Image:TGB_Tutorials_Shooter_PlayerShipCollisionPolygon.jpg


Next, turn on the collision settings and turn off the physics settings:


Image:TGB_Tutorials_Shooter_ShipCollisionSettings.jpg


Once that is complete, we now turn back to the code to let the enemy ships fire missiles, just as we did with the player ship. Open the enemy.cs file, and set the variable containing the missile speed in onLevelLoaded, and also add the fireMissile function:


function EnemyShip::onLevelLoaded(%this, %scenegraph)
{
   // Save the initial X value so we can use it when we respawn this ship
   %this.startX = %this.getPositionX();
   
   // Missile speed
   %this.missileSpeed = -400;
   
   // Spawn this ship
   %this.scenegraph = %scenegraph; // Make sure the scenegraph is available for the fireMissile() function
   %this.spawn();
}


function EnemyShip::fireMissile(%this)
{
   %this.enemyMissile = new t2dStaticSprite()
   {
      scenegraph = %this.scenegraph;
      class = EnemyMissile;
      missileSpeed = %this.missileSpeed;
      enemy = %this;
   };
   
   %this.enemyMissile.fire();
}


This function is almost exactly the same as the PlayerShip::fireMissile() function, and serves exactly the same purpose. Now we need the enemy ship's fireMissile() function to be called from somewhere so that it will actually fire the missile. For now, find the EnemyShip::spawn() function and add this line of code below to the bottom of it, so that whenever an enemy ship spawns it will fire exactly one missile.


   %this.fireMissile();


After you have this code in your enemy.cs file we will need to create another file for the enemy's missile. In your yourProjectName/game/gameScripts folder, create a new file named enemyMissile.cs. Inside this file you need to add the following functions, again almost exactly the same as the playerMissile.cs file, with the major exception of the world limit being greatly expanded on the max X side:


function EnemyMissile::fire(%this)
{
   // Pick the image and set its size
   %this.setImageMap(enemyMissileImageMap);
   %this.setSize(6.25, 3.125);
   
   // Set the missile position to where the enemy ship is
   %this.setPosition(%this.enemy.getPosition());
   %this.setLinearVelocityX(%this.enemy.getLinearVelocityX() - 50);
   
   // Use the camera view bounds as the basis for the missile's world limits
   %cameraViewBounds = sceneWindow2D.getCurrentCameraArea();
   %this.setWorldLimit( kill, getword(%cameraViewBounds, 0) - (%this.getWidth()  / 2),
                              getword(%cameraViewBounds, 1) - (%this.getHeight() / 2),
                              getword(%cameraViewBounds, 2) + 200, // No effective max X limit
                              getword(%cameraViewBounds, 3) + (%this.getHeight() / 2) );
   
   // Turn on collision, but turn off physics
   %this.setCollisionActive( true, true );
   %this.setCollisionPhysics(false, false);
   %this.setCollisionCallback(true);
}


function EnemyMissile::onCollision( %srcObj, %dstObj, %srcRef, %dstRef, %time, %normal, %contactCount, %contacts )
{
   if (%dstObj.class $= "PlayerShip") {
      %srcObj.explode();	
      %dstObj.explode();
   }
}


function EnemyMissile::explode(%this)
{
   %this.safeDelete();
}


This latter two functions are also almost exactly the same as those in the PlayerMissile class, with obvious differences to exchange references from Player to Enemy and vice versa. We also modified the speed of the missile to be a bit faster than what the enemy is, so that it looks like the enemy ship is really shooting at you.


After you get that last function in, save the file, and another exec() in main.cs file to load the enemyMissile.cs file.


   // Exec game scripts
   exec("./gameScripts/game.cs");
   exec("./gameScripts/player.cs");
   exec("./gameScripts/enemy.cs");
   exec("./gameScripts/playerMissile.cs");
   exec("./gameScripts/enemyMissile.cs"); // Add this line


If you run the game, you'll now see your enemy shooting at you whenever it spawns, and if the missile hits the player's ship, then the missile will disappear. The one missing bit is making the ship explode when it is hit by an enemy missile or an enemy ship.



Making the Player Ship Explode


Inside player.cs file you need to tell your player how to respond to being shot with a missile. This is going to be different than the enemy since the player can do different things than the enemy. First and foremost, we are going to add a variable that is going to store whether or not our player is dead. So, in the PlayerShip::onLevelLoaded() function in player.cs file, add this line of code:


function PlayerShip::onLevelLoaded(%this, %scenegraph)
{
   $pShip = %this;
   $pShip.isDead = false; // Add this line here

   ... unchanged code ...
}


This line of code initializes isDead to false, since of course the player is not dead when the game starts.


Next we will need to modify our PlayerShip::Missile() function for the player, since we do not want them to fire if they have been killed. Here is the new function:


function PlayerShip::fireMissile(%this)
{
   // If the player is dead, don't fire a missile
   if (%this.isDead) {
      return;
   }
   
   // Create a missile
   %this.playerMissile = new t2dStaticSprite()
   {
      scenegraph = %this.scenegraph;
      class = PlayerMissile;
      missileSpeed = %this.missileSpeed;
      player = %this;
   };
   
   // Fire the missile
   %this.playerMissile.fire();
}


What these changes actually do is check to see if the player is dead before creating the missile. If the player is dead, this function does nothing.


Next, we need to handle the player's response to collision with the following function:


function PlayerShip::onCollision( %srcObj, %dstObj, %srcRef, %dstRef, %time, %normal, %contactCount, %contacts )
{
   // If the player is already dead, then don't do anything
   if (%this.isDead)
      return;
   
   // If the player hits an enemy ship, explode
   if (%dstObj.class $= "EnemyShip") {
       %srcObj.explode();	
       %dstObj.explode();
   }
}


This function is very similar to the other onCollision() functions we have written so far. It looks for the enemy ship and explodes both it and the player ship if they collide. The only difference is that if isDead is true, (which happens in the explode() function), it won't keep continue through the function, unnecessarily handling collisions. This stops the player from blowing up again while it's already blowing up.


NOTE: You may have noticed that we never added an onCollision() function to the EnemyShip<tt> class. This is because both the <tt>PlayerShip and the PlayerMissile classes are set up to check for collision with the EnemyShip. Since all of the collision is taken care of within those objects, there is no need to add one to the EnemyShip class.


Next we need to create the PlayerShip::explode() function:


function PlayerShip::explode(%this)
{
   %this.isDead = true;
   %this.setEnabled(false);
   %this.schedule(2000, "spawn");
}


What this function does is set isDead to true(meaning the player will not be able to shoot any longer). Then, it sets the player's visibility (setEnable) to false. The last line of code in this function sets a schedule that says basically that in 2000 milliseconds (2 seconds) it will run a function named spawn. Add the spawn function to the end of your file:


function PlayerShip::spawn(%this)
{
   %this.isDead = false;
   %this.setPosition(%this.startX, %this.startY);
   %this.setEnabled(true);
}


This function sets isDead to false (so the player can shoot again). It also sets the player's position to variables startX and startY, two variables containing the initial position of the player's ship which we will define next. Finally, it makes the player visible again. Modify the PlayerShip::onLevelLoaded() function to set startX and startY as shown below:


function PlayerShip::onLevelLoaded(%this, %scenegraph)
{
   $pShip = %this;
   $pShip.isDead = false;
   $pShip.startX = %this.getPositionX();
   $pShip.startY = %this.getPositionY();
   $pShip.hSpeed = 100;
   $pShip.vSpeed = 80;
   $pShip.missileSpeed = 500;
   
   moveMap.bindCmd(keyboard, "up", "pShipUp();", "pShipUpStop();");
   moveMap.bindCmd(keyboard, "down", "pShipDown();", "pShipDownStop();");
   moveMap.bindCmd(keyboard, "left", "pShipLeft();", "pShipLeftStop();");
   moveMap.bindCmd(keyboard, "right", "pShipRight();", "pShipRightStop();");
   moveMap.bindCmd(keyboard, "space", "$pShip.fireMissile();", "");
}


Once you have all of this code saved, run the game. Once you are in, you should fly around and make sure all of your collision is working properly. The player ship should be able to shoot and destroy enemy ships, causing them to respawn. If the player ship collides with the enemy ship, both should be destroyed and respawn. If the player ship is hit by an enemy missile, it should be destroyed and respawn.


Image:TGB_BasicTutorial4_15.jpg
Figure 4.18

Continue on to Part 5 - Adding Particle Effects