2D Space Shooter Part 3: Ships

From GiderosMobile
Revision as of 16:55, 12 July 2023 by Hgy29 (talk | contribs) (Text replacement - "</source" to "</syntaxhighlight")

The objective in a space shooter game is to destroy enemy spaceships, so lets work on that part. There are two kind of ships in the game:

  • The player ship, which is controlled by the player and makes the game end when it is destroyed
  • The enemy ships, which are computer controlled

However both kind of ships have common characteristics: armour, cannons, they can be hit by bullets, etc. Since we want to reuse code as much as possible, we'll create a Ship class to handle common features. But first lets define the actual ships we will have in our game.

Ships models

A ship in our space shooter game

Our ships will have a texture, a few cannons, and a certain armour resistance. We'll define them in a lua table. Create a ships.lua file and insert the ships description:

<source lang="lua"> local SHIPS={ A={ file="ship_1.png", armour=10, size=1, cannons={ { x=49,y=0, type="laser", rate=30 }, }}, B={ file="ship_2.png", armour=15, size=2, cannons={ {x=20,y=30, type="laser", rate=25 }, {x=96,y=30, type="laser", rate=25 }, }}, C={ file="ship_3.png", armour=50, size=3, cannons={ {x=108,y=0, type="missile", rate=80 }, {x=172,y=0, type="missile", rate=80 }, {x=40,y=105, type="laser", rate=20, angle=-15 }, {x=240,y=105, type="laser", rate=20, angle=15 }, }}, Z={ file="ship_0.png", armour=100, size=2, cannons={ {x=22,y=45, type="laser", rate=15 }, {x=155,y=45, type="laser", rate=15 }, }}, }

SHIP_SIZE=512/6 --Logical size of a 1 unit ship </syntaxhighlight> Each key in our table is a ship type. I choose single letter type names to make it easier to create our levels later. Each entry describe the ship characteristics:

  • the texture file to use
  • the armour strength
  • the relative size of the ship
  • an array of cannons. Cannons have a position and angle in the source image, a bullet type and a fire rate

We'll want to arrange our ships on a grid when they arrive on screen, as if they were part of a wing. To do so, we need our ships to be scaled appropriately to fit cells. That is what the relative size field is for, it will tell how many cells this ship is supposed to span. The final line of the above code block specifies the size of a grid cell. We computed it so that five small ships can span the screen width, but divided by 6 to leave a bit of margin.

The Ship class

We'll use the same technique as the Background, and have our Ship class extend Gideros Pixel. It will look like this: <source lang="lua"> Ship=Core.class(Pixel,function (type) end)

function Ship:init(type) type=type or "Z" local ship_def=SHIPS[type] assert(ship_def,"No such ship type: "..type) -- Set up our ship based on its definition --- ... -- Add to actors list ACTORS[self]=true end

function Ship:destroy() -- Clean up ACTORS[self]=nil -- Remove from screen self:removeFromParent() end

function Ship:tick(delay) -- Do whatever needs to be done on the game loop (move, fire cannons, etc) end </syntaxhighlight> The 'Ship:init' method will look up the given ship type from the ships definition list, defaulting to the "Z" type if no type was supplied, then initialize itself from that ship description. As a last step of its initialization, it will register itself to a global list of actors in the game. The idea here is to have each ship take care of itself without cluttering the main game loop. For now, append the above code block to your ships.lua file. We will complete this skeleton shortly.

For now switch back to main.lua file, some changes must be made already. Our game loop must call the 'tick' method of each object in the ACTORS list. While we are at it, let's create the collision world with cbump plugin, we will need it shortly. Don't forget to add the 'bump' plugin to your project settings!

Add this early in your main.lua code: <source lang="lua"> -- Create the collision world BUMP=require "cbump" BUMP_WORLD=BUMP.newWorld() -- A list of all objects that should receive frame ticks ACTORS={} </syntaxhighlight>

And the following code inside your game loop: <source lang="lua"> for k,_ in pairs(ACTORS) do k:tick(1) end </syntaxhighlight>

The game loop code should look like this now: <source lang="lua"> -- This is our game loop stage:addEventListener(Event.ENTER_FRAME,function () background:advance(1) for k,_ in pairs(ACTORS) do k:tick(1) end end) </syntaxhighlight>

Ship code

Our code should still run so far, but we won't see anything new. Lets complete our ship code and show it on screen.

First complete the 'Ship:init' method: <source lang="lua"> function Ship:init(type) type=type or "Z" local ship_def=SHIPS[type] assert(ship_def,"No such ship type: "..type) -- Load the texture local texture=Texture.new("gfx/"..ship_def.file,true) -- Compute ship scale local tw,th=texture:getWidth(),texture:getHeight() local size=(ship_def.size or 1)*SHIP_SIZE local scale=size/(tw<>th) -- Set up texture and dimensions self:setTexture(texture) self:setDimensions(tw*scale,th*scale) -- Position the ship by its center self:setAnchorPoint(0.5,0.5) -- Keep a few variables in our object self.scale=scale self.armour=ship_def.armour -- Set up cannons self.cannons={} for k,cdef in ipairs(ship_def.cannons) do --self.cannons[k]=Cannon.new(cdef,scale,self) end -- Add to collision world BUMP_WORLD:add(self,0,0,tw*scale,th*scale) -- Add to actors list ACTORS[self]=true end </syntaxhighlight> A few remarks:

  • I used the <> operator, which is something specific to gideros. It stands for 'take the maximum of the two values'. Likewise, the >< operator can be used to take the lowest value. While it makes the lua code incompatible with other engines, it also makes it faster and more difficult to recover from compiled code.
  • The cannons will be handled by their own objects. I left that line commented because we'll that that later.
  • The collision rectangle in bump world is initially placed at coordinate 0,0 (but width and height are set up correctly). We will update them in the 'Ship:tick' method below.

Our 'tick' method will update the collision world according to our ship position, and fire the cannons if appropriate. <source lang="lua"> function Ship:tick(delay) local shipax,shipay=self:getAnchorPosition() local shipx,shipy=self:getPosition() BUMP_WORLD:update(self,shipx-shipax,shipy-shipay) if self.fire then for _,cannon in ipairs(self.cannons) do cannon:fire(delay) end end end </syntaxhighlight>

We must not forget to remove our ship from the collision world when it gets destroyed. Add the following lines inside the 'Ship:destroy' method. <source lang="lua"> -- Remove from collision world BUMP_WORLD:remove(self) </syntaxhighlight>

Showing our ship

We have done a lot of hidden work in this chapter, to see a bit of evolution from the previous chapter, lets add our ship on screen.

In main.lua add the following code at the end: <source lang="lua"> -- A graphic layer for all enemies ENEMIES=Sprite.new() stage:addChild(ENEMIES)

local player=Ship.new() player:setPosition(256,SCR_BOTTOM-128) stage:addChild(player)

-- A graphic layer for all bullets BULLETS=Sprite.new() stage:addChild(BULLETS) </syntaxhighlight>

With this code, we are declaring the enemy layer, the player ship and the bullets layer. We place our ship centered at the bottom of the screen. Now we just need to add our ship images to our project (under the gfx folder), and see the result.

2D Space Shooter Part 4: Player