User Tools

Site Tools


====== Collisions ====== [[tutorial:index|Tutorial index]] ==== Introduction ==== 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}}. ==== Creating shapes ==== 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. {{:tutorial:collisions:collisions-editor.png?direct&400}} 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. {{:tutorial:collisions:collisions-editor-player.png?direct&400}} Add a polygon for the asteroid in the same way. {{:tutorial:collisions:collisions-editor-asteroid.png?direct&400}} Save the shapes as a list named //Shapes.shapes// {{:tutorial:collisions:collisions-editor-save.png?direct&400}} ==== Loading shapes ==== Add the [[|phxShape]] and [[|phxSprite]] unit to the uses list. <code pascal> 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; </code> Add a instance variable for the shape list to the game class. <code pascal> TGame = class(TPHXApplication) private Device : TPHXDevice; Canvas : TPHXCanvas; Timer : TPHXTimer; Input : TPHXInput; Images : TPHXImageList; Fonts : TPHXFontList; Shapes : TPHXShapeList; ... end; </code> <code pascal> procedure TGame.Init; begin ... // Create the shape list Shapes:= TPHXShapeList.Create; // Load the shapes from disk Shapes.LoadFromFile('Shapes.shapes'); end; </code> ==== Create sprites ==== Add a variable for the sprite engine and for our player sprite and a procedure for creating the sprites <code pascal> TGame = class(TPHXApplication) private ... Sprites : TPHXSpriteEngine; Player : TPHXSprite; procedure CreateSprites; ... end; </code> Create the sprites, for more information about the properties of the sprites see [[|TPHXSprite]] in the API documentation. <code pascal> 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; </code> Add a call to the CreateSprites procedure from the TGame.Init procedure <code pascal> procedure TGame.Init; begin ... // Create the sprites CreateSprites; end; </code> ==== Update the sprites ==== Next we have to update the sprite engine using the [[|Update]] method of the sprite engine. <code pascal> 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; </code> To make the player move we use the rotate and move helpers from the sprite class <code pascal> 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; </code> We want the screen to be centered on the player, this can be done with the [[|CenterOn]] function of the sprite camera. <code pascal> procedure TGame.Update; begin ... // Making the sprite engine follow the player Sprites.Camera.CenterOn(Player); end; </code> ==== Rendering sprites ==== 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. <code pascal> 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; </code> When running the application it should look like this {{:tutorial:collisions:collisions-running-sprites.png?direct&400|}} ==== Collision testing ==== 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. <code pascal> 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[0].TextOut(4,4 + Fonts[0].Height * Index, Collisions[Index].B.Name); end; finally Collisions.Free; end; ... end; </code> Now the name of the collided sprites should be rendered {{:tutorial:collisions:collisions-running-collision.png?direct&400|}} ==== Collision masking ==== 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. <code pascal> // Mark asteroids as static Asteroid.Mode:= cmStatic; ... // Mark the player as dynamic (this is the default value) Player.Mode:= cmDynamic; </code> It is also possible to only test for collisions with a specific set of sprites using the [[|TPHXSprite.Group]] property. <code pascal> // 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); </code> To see the collision masking in action see the Platformer demo. ==== Shooting ==== 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. {{:tutorial:collisions:collisions-bullet-hardpoints.png?direct&400}} Now we have to add the image for the bullet sprite. {{:tutorial:resources:bullet.png?direct|}} 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). {{:tutorial:collisions:collisions-bullet-import.png?direct&400}} 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" {{:tutorial:collisions:collisions-bullet-place.png?direct&400}} Next start the shape editor open our //Shapes.shapes// file and add a circle for the bullet with a radius of 4. {{:tutorial:collisions:collisions-bullet-shape.png?direct&400}} Save the list again ==== Bullet class ==== 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. <code pascal> TBullet = class(TPHXSprite) public procedure Update(const DeltaTime: Double); override; procedure Collided(Sprite: TPHXSprite); override; end; </code> In the update function we move the bullet forward and kills it after one second. <code pascal> 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; </code> When the bullet has collided with another sprite we will kill the both bullet and the sprite it has collided with. <code pascal> 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; </code> ==== Fire a bullet ==== 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. <code pascal> 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; </code> Then its just a matter of calling the Fire function when a button is pressed. <code pascal> procedure TGame.Update; begin .. // Fire a bullet if isButton1 in Input.States then begin Fire; Input.States:= Input.States - [isButton1]; end; end; </code> Run the application. {{:tutorial:collisions:collisions-running-bullets.png?direct&400|}} ==== Adding cooldown between each shot ==== 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 <code pascal> TGame = class(TPHXApplication) private ... Cooldown: Single; end; </code> Then replace the fire logic with this <code pascal> 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; </code> 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. [[tutorial:index|Back to tutorial index]]

tutorial/collision.txt · Last modified: 2013/08/31 09:04 by amnoxx