In this tutorial we will add collision detection to our game. To help with this we will use the sprite engine to manage our game objects.
The api documumentation for the sprite engine can be found here
You can download the source for this tutorial here.
The first thing we have to do is to create the shapes that will be used for collision testing. Start the image editor and load Shooter.phximg from the previous tutorials.
Now start the shape editor from the tool menu.
Select relative to pattern Player and add a new polygon shape. Rename this shape to Player and press the Create from image button to create a convex polygon that surrounds all the visible pixels of the pattern.
Add a polygon for the asteroid in the same way.
Save the shapes as a list named Shapes.shapes
Add the phxShape and phxSprite unit to the uses list.
uses SysUtils, // Basic phoenix types phxTypes, // Contains phoenix utility classes phxClasses, // Math functions phxMath, // Device phxDevice, // Contains the application framework phxApplication, // Contains the canvas class for rendering 2D primitives phxCanvas, // Image classes phxImage, // Bitmap font phxFont, // Input framework phxInput, // Shapes for collision testing phxShape, // Sprite engine phxSprite;
Add a instance variable for the shape list to the game class.
TGame = class(TPHXApplication) private Device : TPHXDevice; Canvas : TPHXCanvas; Timer : TPHXTimer; Input : TPHXInput; Images : TPHXImageList; Fonts : TPHXFontList; Shapes : TPHXShapeList; ... end;
procedure TGame.Init; begin ... // Create the shape list Shapes:= TPHXShapeList.Create; // Load the shapes from disk Shapes.LoadFromFile('Shapes.shapes'); end;
Add a variable for the sprite engine and for our player sprite and a procedure for creating the sprites
TGame = class(TPHXApplication) private ... Sprites : TPHXSpriteEngine; Player : TPHXSprite; procedure CreateSprites; ... end;
Create the sprites, for more information about the properties of the sprites see TPHXSprite in the API documentation.
procedure TGame.CreateSprites; var Index : Integer; var Asteroid: TPHXSprite; begin // Create the sprite engine Sprites:= TPHXSpriteEngine.Create(Device); Sprites.Images:= Images; Sprites.Shapes:= Shapes; // Create the asteroid sprites for Index := 1 to 10 do begin Asteroid:= TPHXSprite.Create(Sprites); Asteroid.Name := 'Asteroid ' + IntToStr(Index); Asteroid.X := 20 + Random * (Device.Width - 40); Asteroid.Y := 20 + Random * (Device.Height - 40); Asteroid.Image := 'Shooter'; Asteroid.Shape := 'Asteroid'; Asteroid.Pattern := 'Asteroid'; Asteroid.Collider:= True; Asteroid.Parent := Sprites.Root; end; // Create the player sprite Player:= TPHXSprite.Create(Sprites); Player.Name := 'Player'; Player.X := 100; Player.Y := 100; Player.Image := 'Shooter'; Player.Shape := 'Player'; Player.Pattern := 'Player'; Player.Collider:= True; Player.Parent := Sprites.Root; // Initialize the sprite engine Sprites.Initialize; end;
Add a call to the CreateSprites procedure from the TGame.Init procedure
procedure TGame.Init; begin ... // Create the sprites CreateSprites; end;
Next we have to update the sprite engine using the Update method of the sprite engine.
procedure TGame.Update; begin // Update the device Device.Update; // Update the timer Timer.Update; // Update the input Input.Update; // Update the sprite engine Sprites.Update(Timer.FrameTime); end;
To make the player move we use the rotate and move helpers from the sprite class
procedure TGame.Update; begin ... // Rotate left with 90 degrees per second if isLeft in Input.States then begin Player.RotateLeft(90 * Timer.FrameTime); end; // Rotate rightwith 90 degrees per second if isRight in Input.States then begin Player.RotateRight(90 * Timer.FrameTime); end; // Move forward along the rotation with 200 pixels per second if isUp in Input.States then begin Player.MoveForward(200 * Timer.FrameTime); end; // Move backward along the rotation with 200 pixels per second if isDown in Input.States then begin Player.MoveBackward(200 * Timer.FrameTime); end; end;
We want the screen to be centered on the player, this can be done with the CenterOn function of the sprite camera.
procedure TGame.Update; begin ... // Making the sprite engine follow the player Sprites.Camera.CenterOn(Player); end;
To render the sprites we only have to call the Render function of the sprite engine.
We will also render the shapes and the bounding boxes of all sprites. You don't have to add this but it might help to visualize what is happening.
procedure TGame.Render; begin // Clear the back buffer Device.Clear; // Render the sprites Sprites.Render; // Render the shapes for the sprites Sprites.RenderShapes(Canvas, clrWhite); // Render the bounding boxes for the sprites Sprites.RenderBounds(Canvas, clrSilver); // Flush the canvas Canvas.Flush; // Flip the front and back buffers to show the scene Device.Flip; end;
When running the application it should look like this
When calling the TPHXSpriteEngine.Update function all collisions are updated and the virtual TPHXSprite.Collided function for the collided sprites are called. But as we are using the sprite class without creating a subclass of it we cant use this function.
Instead we can use the TPHXSpriteEngine.Collide function to query for collisions. This function collides one sprite against all other sprites and fills a collision list with all the collisions.
In this case we just use this for rendering the name of the collided sprites.
procedure TGame.Render; var Collisions: TPHXSpriteCollisionList; var Index : Integer; begin ... Collisions:= TPHXSpriteCollisionList.Create; try Sprites.Collide(Player, Collisions); for Index:= 0 to Collisions.Count-1 do begin Fonts.TextOut(4,4 + Fonts.Height * Index, Collisions[Index].B.Name); end; finally Collisions.Free; end; ... end;
Now the name of the collided sprites should be rendered
In the code above all sprites are tested against all other sprites for collisions. This will be quite slow when you add a lot of sprites. But we don't need the asteroids to collide against each other. To disable this we can change their TPHXSprite.Mode to static.
// Mark asteroids as static Asteroid.Mode:= cmStatic; ... // Mark the player as dynamic (this is the default value) Player.Mode:= cmDynamic;
It is also possible to only test for collisions with a specific set of sprites using the TPHXSprite.Group property.
// Make the asteroids belong to group 2 Asteroid.Group:= cgGroup2; ... // Make the player belong to group 1 Player.Group:= cgGroup1; .. // Only collide the player against asteroids Sprites.Collide(Player, Collisions, cgGroup2);
To see the collision masking in action see the Platformer demo.
Drawing asteroids is all fun, but what is even more fun is to blow them up! Lets add some code for firring bullets from our ship.
First we have to add some weapon hardpoints to the ship sprite, start the image editor and open Shooter.phximg and select the Tags page (or cheat and download the image here.
Add two tags called “Hardpoint1” and “Hardpoint” and move them to the front edge of each wing of the ship.
Now we have to add the image for the bullet sprite.
Download the image and use the import tool from the pattern toolbar (This is only visible if you have the pattern page open, if you are still on the tags page it will be hidden).
When pressing the button you get a open dialog where you can select the bullet image, place the image in a empty position by clicking with the mouse and pres enter to insert the bullet image into the image. A pattern will be automatically created with the same name as the image. Make sure it is named “Bullet”
Next start the shape editor open our Shapes.shapes file and add a circle for the bullet with a radius of 4.
Save the list again
To create bullets we add a new class that contains the logic of the bullet. We need to override the Update and the Collided functions.
TBullet = class(TPHXSprite) public procedure Update(const DeltaTime: Double); override; procedure Collided(Sprite: TPHXSprite); override; end;
In the update function we move the bullet forward and kills it after one second.
procedure TBullet.Update(const DeltaTime: Double); begin inherited; // Move forward with 400 pixels per second MoveForward(250 * DeltaTime); // Kill the bullet after one second if Time > 1 then Kill; end;
When the bullet has collided with another sprite we will kill the both bullet and the sprite it has collided with.
procedure TBullet.Collided(Sprite: TPHXSprite); begin inherited; // We only have asteroids the player and bullets in the sprite list so when we have collided // with something other then the player its safe to assume its a asteroid we have collided with. // A better way is to make a class for asteroids and test with if Sprite is TAsteroid instead. if(Sprite.Name <> 'Player') then begin // Kill the bullet Kill; // Kill the asteroid Sprite.Kill; end; end;
To fire the bullet class create a new function and add it to the game. We will use the TPHXSprite.AttatchTo function to move the sprite to the tags of the player sprite.
Note that the rotation of the tags and the sprite rotation is considered to calculate the rotation of the bullet sprite. If you change the rotation of the hardpoint tags in the image editor you can get the player to shoot in different directions.
procedure TGame.Fire; var Bullet: TPHXSprite; begin // Create the bullet sprite for the first hardpoint Bullet:= TBullet.Create(Sprites); Bullet.Name := 'Bullet'; Bullet.Image := 'Shooter'; Bullet.Pattern := 'Bullet'; Bullet.Shape := 'Bullet'; Bullet.Collider:= True; Bullet.Parent := Sprites.Root; // Attatch the first bullet to the "Hardpoint1" tag of the image Bullet.AttatchTo(Player, 'Hardpoint1'); // Create the bullet sprite for the second hardpoint Bullet:= TBullet.Create(Sprites); Bullet.Name := 'Bullet'; Bullet.Image := 'Shooter'; Bullet.Pattern := 'Bullet'; Bullet.Shape := 'Bullet'; Bullet.Collider:= True; Bullet.Parent := Sprites.Root; // Attatch the second bullet to the "Hardpoint2" tag of the image Bullet.AttatchTo(Player, 'Hardpoint2'); end;
Then its just a matter of calling the Fire function when a button is pressed.
procedure TGame.Update; begin .. // Fire a bullet if isButton1 in Input.States then begin Fire; Input.States:= Input.States - [isButton1]; end; end;
Run the application.
The player now shoots two bullet every time you press the fire button. If we want to fire while holding the button we have to add a cooldown between each shot.
Create a variable for the cooldown
TGame = class(TPHXApplication) private ... Cooldown: Single; end;
Then replace the fire logic with this
procedure TGame.Update; begin .. Cooldown:= Cooldown - Timer.FrameTime; if (isButton1 in Input.States) and (Cooldown < 0) then begin Fire; // We must wait 0.2 seconds before fireing again Cooldown:= 0.2; end; end;
A better way then the above would be to add this code to a TPlayer class, but I will leave that as a exercise for the reader.