Difference between revisions of "2D Space Shooter Part 3: Ships"
m (Text replacement - "<source" to "<syntaxhighlight") |
|||
(9 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | The objective in a space shooter game is to destroy enemy spaceships, so | + | __TOC__ |
+ | The objective in a space shooter game is to destroy enemy spaceships, so let's work on that part. | ||
+ | |||
There are two kind of ships in the game: | 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 | + | 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 will create a Ship class to handle common features. But first let's define the actual ships we will have in our game. |
== Ships models == | == Ships models == | ||
− | |||
[[File:2D Spaceshooter Ship 3.png|thumb|A ship in our space shooter game]] | [[File:2D Spaceshooter Ship 3.png|thumb|A ship in our space shooter game]] | ||
− | + | Download the '''[[Media:2D_Spaceshooter_Ships.zip|ship images zip file]]''', unzip it and add each ship image to your project under the gfx folder. | |
− | |||
+ | Our ships will have a texture, a few cannons, and a certain armour resistance. We will define them in a lua table. Create a '''ships.lua''' file and insert the ships description: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | local SHIPS={ | + | local SHIPS = { |
− | A={ file="ship_1.png", armour=10, size=1, | + | A = { file="ship_1.png", armour=10, size=1, -- SHIP A |
− | cannons={ | + | cannons={ |
− | { x= | + | { x=0, y=-122/2, type="laser", rate=10*8 }, -- single cannon |
}}, | }}, | ||
− | B={ file="ship_2.png", armour=15, size=2, | + | B={ file="ship_2.png", armour=15, size=2, -- SHIP B |
cannons={ | cannons={ | ||
− | {x= | + | {x=-115/5, y=-161/2, type="laser", rate=8*8 }, -- double cannon |
− | {x= | + | {x=115/5, y=-161/2, type="laser", rate=8*8 }, -- double cannon |
}}, | }}, | ||
− | C={ file="ship_3.png", armour=50, size=3, | + | C={ file="ship_3.png", armour=50, size=3, -- BOSS SHIP |
cannons={ | cannons={ | ||
− | {x= | + | {x=-283/3, y=0, type="missile", rate=6*8 }, -- quadruple cannon! |
− | {x= | + | {x=283/3, y=0, type="missile", rate=6*8 }, -- quadruple cannon! |
− | {x=40,y= | + | {x=-40, y=-357/2, type="laser", rate=4*8, angle=-40 }, -- quadruple cannon! |
− | {x= | + | {x=40, y=-357/2, type="laser", rate=4*8, angle=40 }, -- quadruple cannon! |
}}, | }}, | ||
− | Z={ file="ship_0.png", armour=100, size= | + | Z={ file="ship_0.png", armour=100, size=1, -- PLAYER SHIP |
cannons={ | cannons={ | ||
− | {x= | + | {x=-68, y=0, type="laser", rate=2*8 }, -- double cannon |
− | {x= | + | {x=68, y=0, type="laser", rate=2*8 }, -- double cannon |
}}, | }}, | ||
} | } | ||
− | SHIP_SIZE=512/6 -- | + | SHIP_SIZE = 512/6 -- logical size of a 1 unit ship |
</syntaxhighlight> | </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: | + | |
+ | 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 texture file to use | ||
* the armour strength | * the armour strength | ||
Line 45: | Line 47: | ||
* an array of cannons. Cannons have a position and angle in the source image, a bullet type and a fire rate | * an array of cannons. Cannons have a position and angle in the source image, a bullet type and a fire rate | ||
− | We | + | We want to arrange our ships in a grid when they arrive on screen, as if they were part of a wave. To do so, we need our ships to be scaled appropriately to fit the 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 == | == The Ship class == | ||
+ | We will use the same technique as the Background, and have our Ship class extend Gideros Pixel. | ||
− | + | Below the ships definition we create our Ship class: | |
− | |||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | Ship=Core.class(Pixel,function (type) end) | + | Ship = Core.class(Pixel, function(type) end) |
function Ship:init(type) | function Ship:init(type) | ||
− | type=type or "Z" | + | type = type or "Z" -- "A", "B", "C" = enemies, "Z" = player |
− | local ship_def=SHIPS[type] | + | local ship_def = SHIPS[type] |
− | assert(ship_def,"No such ship type: "..type) | + | assert(ship_def, "No such ship type: "..type) |
− | -- | + | -- set up our ship based on its definition |
--- ... | --- ... | ||
− | -- | + | -- add to actors list |
− | ACTORS[self]=true | + | ACTORS[self] = true |
end | end | ||
function Ship:destroy() | function Ship:destroy() | ||
− | + | ACTORS[self] = nil -- remove from ACTORS list | |
− | ACTORS[self]=nil | + | self:removeFromParent() -- remove from screen |
− | |||
− | self:removeFromParent() | ||
end | end | ||
function Ship:tick(delay) | function Ship:tick(delay) | ||
− | -- | + | -- do whatever needs to be done on the game loop (move, fire cannons, etc) |
end | end | ||
</syntaxhighlight> | </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. | 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. | |
+ | |||
+ | |||
+ | Now switch back to main.lua file, some changes will be made. Our game loop must call the 'tick' method of each object in the ACTORS list. | ||
Add this early in your main.lua code: | Add this early in your main.lua code: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | -- | + | -- a list of all ship objects that should receive frame ticks |
− | + | ACTORS = {} | |
− | |||
− | |||
− | ACTORS={} | ||
</syntaxhighlight> | </syntaxhighlight> | ||
And the following code inside your game loop: | And the following code inside your game loop: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | for k,_ in pairs(ACTORS) do | + | for k, _ in pairs(ACTORS) do |
k:tick(1) | k:tick(1) | ||
end | end | ||
Line 98: | Line 98: | ||
The game loop code should look like this now: | The game loop code should look like this now: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | -- | + | -- this is our game loop |
− | stage:addEventListener(Event.ENTER_FRAME,function () | + | stage:addEventListener(Event.ENTER_FRAME, function() |
background:advance(1) | background:advance(1) | ||
− | for k,_ in pairs(ACTORS) do | + | for k, _ in pairs(ACTORS) do |
k:tick(1) | k:tick(1) | ||
end | end | ||
Line 108: | Line 108: | ||
== Ship code == | == Ship code == | ||
− | Our code should still run so far, but we won't see anything new. | + | Our code should still run so far, but we won't see anything new. Let's complete our ship code and show it on screen. |
First complete the 'Ship:init' method: | First complete the 'Ship:init' method: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
function Ship:init(type) | function Ship:init(type) | ||
− | type=type or "Z" | + | type = type or "Z" -- "A", "B", "C" = enemies, "Z" = player |
− | local ship_def=SHIPS[type] | + | local ship_def = SHIPS[type] |
− | assert(ship_def,"No such ship type: "..type) | + | assert(ship_def, "No such ship type: "..type) |
− | -- | + | -- load the texture |
− | local texture=Texture.new("gfx/"..ship_def.file,true) | + | local texture = Texture.new("gfx/"..ship_def.file, true) |
− | -- | + | -- compute ship scale |
− | local tw,th=texture:getWidth(),texture:getHeight() | + | local tw, th = texture:getWidth(), texture:getHeight() |
− | local size=(ship_def.size or 1)*SHIP_SIZE | + | local size = (ship_def.size or 1)*SHIP_SIZE |
− | local scale=size/(tw<>th) | + | local scale = size/(tw<>th) |
− | -- | + | -- set up texture and dimensions |
self:setTexture(texture) | self:setTexture(texture) | ||
− | self:setDimensions(tw*scale,th*scale) | + | self:setDimensions(tw*scale, th*scale) |
− | -- | + | -- position the ship by its center |
− | self:setAnchorPoint(0.5,0.5) | + | self:setAnchorPoint(0.5, 0.5) |
− | -- | + | -- keep a few variables in our object |
− | self.scale=scale | + | self.scale = scale |
− | self.armour=ship_def.armour | + | self.armour = ship_def.armour |
− | -- | + | -- by default the ships are enemy ships |
− | self.cannons={} | + | self.isplayer = false |
− | for k,cdef in ipairs(ship_def.cannons) do | + | if type == "Z" then self.isplayer = true end -- Z = player ship |
− | --self.cannons[k]=Cannon.new(cdef,scale,self) | + | -- set up cannons |
+ | self.cannons = {} | ||
+ | for k, cdef in ipairs(ship_def.cannons) do | ||
+ | --self.cannons[k] = Cannon.new(cdef, scale, self) | ||
end | end | ||
− | -- | + | -- add to actors list |
− | + | ACTORS[self] = true | |
− | |||
− | ACTORS[self]=true | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
A few remarks: | A few remarks: | ||
− | * I used the <> operator, which is something specific to | + | * 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 do that later |
− | * | + | * by default all the ships are flagged as enemy ships ''self.isplayer = false'' but for the player ship (Z) we set it to true. This flag will be used for the bullets and collision checking! |
− | Our 'tick' method will | + | Our 'tick' method will fire the cannons if appropriate: |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
function Ship:tick(delay) | function Ship:tick(delay) | ||
− | |||
− | |||
− | |||
if self.fire then | if self.fire then | ||
− | for _,cannon in ipairs(self.cannons) do | + | for _, cannon in ipairs(self.cannons) do |
cannon:fire(delay) | cannon:fire(delay) | ||
end | end | ||
Line 160: | Line 159: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | We | + | == Showing our ship == |
+ | We have done a lot of hidden work in this chapter, to see a bit of evolution from the previous chapter, let's add our ship on screen. | ||
+ | |||
+ | In main.lua, above the game loop, add the following code: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | + | -- a graphic layer for all enemies | |
− | + | ENEMIES_LAYER = Sprite.new() | |
+ | stage:addChild(ENEMIES_LAYER) | ||
+ | |||
+ | -- the player ship | ||
+ | local player = Ship.new() | ||
+ | player:setPosition((SCR_RIGHT-SCR_LEFT)/2, SCR_BOTTOM-128) | ||
+ | stage:addChild(player) | ||
+ | |||
+ | -- a graphic layer for all bullets | ||
+ | BULLETS_LAYER = Sprite.new() | ||
+ | stage:addChild(BULLETS_LAYER) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | With this code, we are declaring the enemies layer, the player ship and the bullets layer and we place our ship centered at the bottom of the screen. | |
− | |||
− | + | Your main.lua should look something like this: | |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | -- | + | -- a list of all ship objects that should receive frame ticks |
− | + | ACTORS = {} | |
− | stage:addChild( | + | |
+ | -- compute screen bounds | ||
+ | SCR_LEFT, SCR_TOP, SCR_RIGHT, SCR_BOTTOM = application:getLogicalBounds() | ||
+ | |||
+ | -- create the background object with the screen size | ||
+ | -- width should be 512 per project settings, but we'll compute it anyway to avoid relying on constants | ||
+ | local background = Background.new(SCR_RIGHT-SCR_LEFT, SCR_BOTTOM-SCR_TOP) | ||
+ | -- add it to stage | ||
+ | stage:addChild(background) | ||
+ | background:setY(SCR_TOP) | ||
+ | |||
+ | -- a graphic layer for all enemies | ||
+ | ENEMIES_LAYER = Sprite.new() | ||
+ | stage:addChild(ENEMIES_LAYER) | ||
− | local player=Ship.new() | + | -- the player ship |
− | player:setPosition( | + | local player = Ship.new() |
+ | player:setPosition((SCR_RIGHT-SCR_LEFT)/2, SCR_BOTTOM-128) | ||
stage:addChild(player) | stage:addChild(player) | ||
− | -- | + | -- a graphic layer for all bullets |
− | + | BULLETS_LAYER = Sprite.new() | |
− | stage:addChild( | + | stage:addChild(BULLETS_LAYER) |
+ | |||
+ | -- 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> | </syntaxhighlight> | ||
− | + | Let's see the result. | |
{{#widget:GApp|app=SpaceShooter_SH1.GApp|width=320|height=480|plugins=bump}} | {{#widget:GApp|app=SpaceShooter_SH1.GApp|width=320|height=480|plugins=bump}} | ||
− | [[2D Space Shooter Part 4: Player]] | + | |
+ | Prev.: [[2D Space Shooter Part 2: Background]]</br> | ||
+ | '''Next: [[2D Space Shooter Part 4: Player]]''' | ||
+ | |||
+ | |||
+ | '''[[Tutorial - Making a 2D space shooter game]]''' | ||
+ | {{GIDEROS IMPORTANT LINKS}} |
Latest revision as of 22:08, 15 November 2023
The objective in a space shooter game is to destroy enemy spaceships, so let's 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 will create a Ship class to handle common features. But first let's define the actual ships we will have in our game.
Ships models
Download the ship images zip file, unzip it and add each ship image to your project under the gfx folder.
Our ships will have a texture, a few cannons, and a certain armour resistance. We will define them in a lua table. Create a ships.lua file and insert the ships description:
local SHIPS = {
A = { file="ship_1.png", armour=10, size=1, -- SHIP A
cannons={
{ x=0, y=-122/2, type="laser", rate=10*8 }, -- single cannon
}},
B={ file="ship_2.png", armour=15, size=2, -- SHIP B
cannons={
{x=-115/5, y=-161/2, type="laser", rate=8*8 }, -- double cannon
{x=115/5, y=-161/2, type="laser", rate=8*8 }, -- double cannon
}},
C={ file="ship_3.png", armour=50, size=3, -- BOSS SHIP
cannons={
{x=-283/3, y=0, type="missile", rate=6*8 }, -- quadruple cannon!
{x=283/3, y=0, type="missile", rate=6*8 }, -- quadruple cannon!
{x=-40, y=-357/2, type="laser", rate=4*8, angle=-40 }, -- quadruple cannon!
{x=40, y=-357/2, type="laser", rate=4*8, angle=40 }, -- quadruple cannon!
}},
Z={ file="ship_0.png", armour=100, size=1, -- PLAYER SHIP
cannons={
{x=-68, y=0, type="laser", rate=2*8 }, -- double cannon
{x=68, y=0, type="laser", rate=2*8 }, -- double cannon
}},
}
SHIP_SIZE = 512/6 -- logical size of a 1 unit ship
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 want to arrange our ships in a grid when they arrive on screen, as if they were part of a wave. To do so, we need our ships to be scaled appropriately to fit the 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 will use the same technique as the Background, and have our Ship class extend Gideros Pixel.
Below the ships definition we create our Ship class:
Ship = Core.class(Pixel, function(type) end)
function Ship:init(type)
type = type or "Z" -- "A", "B", "C" = enemies, "Z" = player
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()
ACTORS[self] = nil -- remove from ACTORS list
self:removeFromParent() -- remove from screen
end
function Ship:tick(delay)
-- do whatever needs to be done on the game loop (move, fire cannons, etc)
end
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.
Now switch back to main.lua file, some changes will be made. Our game loop must call the 'tick' method of each object in the ACTORS list.
Add this early in your main.lua code:
-- a list of all ship objects that should receive frame ticks
ACTORS = {}
And the following code inside your game loop:
for k, _ in pairs(ACTORS) do
k:tick(1)
end
The game loop code should look like this now:
-- 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)
Ship code
Our code should still run so far, but we won't see anything new. Let's complete our ship code and show it on screen.
First complete the 'Ship:init' method:
function Ship:init(type)
type = type or "Z" -- "A", "B", "C" = enemies, "Z" = player
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
-- by default the ships are enemy ships
self.isplayer = false
if type == "Z" then self.isplayer = true end -- Z = player ship
-- 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 actors list
ACTORS[self] = true
end
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 do that later
- by default all the ships are flagged as enemy ships self.isplayer = false but for the player ship (Z) we set it to true. This flag will be used for the bullets and collision checking!
Our 'tick' method will fire the cannons if appropriate:
function Ship:tick(delay)
if self.fire then
for _, cannon in ipairs(self.cannons) do
cannon:fire(delay)
end
end
end
Showing our ship
We have done a lot of hidden work in this chapter, to see a bit of evolution from the previous chapter, let's add our ship on screen.
In main.lua, above the game loop, add the following code:
-- a graphic layer for all enemies
ENEMIES_LAYER = Sprite.new()
stage:addChild(ENEMIES_LAYER)
-- the player ship
local player = Ship.new()
player:setPosition((SCR_RIGHT-SCR_LEFT)/2, SCR_BOTTOM-128)
stage:addChild(player)
-- a graphic layer for all bullets
BULLETS_LAYER = Sprite.new()
stage:addChild(BULLETS_LAYER)
With this code, we are declaring the enemies layer, the player ship and the bullets layer and we place our ship centered at the bottom of the screen.
Your main.lua should look something like this:
-- a list of all ship objects that should receive frame ticks
ACTORS = {}
-- compute screen bounds
SCR_LEFT, SCR_TOP, SCR_RIGHT, SCR_BOTTOM = application:getLogicalBounds()
-- create the background object with the screen size
-- width should be 512 per project settings, but we'll compute it anyway to avoid relying on constants
local background = Background.new(SCR_RIGHT-SCR_LEFT, SCR_BOTTOM-SCR_TOP)
-- add it to stage
stage:addChild(background)
background:setY(SCR_TOP)
-- a graphic layer for all enemies
ENEMIES_LAYER = Sprite.new()
stage:addChild(ENEMIES_LAYER)
-- the player ship
local player = Ship.new()
player:setPosition((SCR_RIGHT-SCR_LEFT)/2, SCR_BOTTOM-128)
stage:addChild(player)
-- a graphic layer for all bullets
BULLETS_LAYER = Sprite.new()
stage:addChild(BULLETS_LAYER)
-- 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)
Let's see the result.
Prev.: 2D Space Shooter Part 2: Background
Next: 2D Space Shooter Part 4: Player
Tutorial - Making a 2D space shooter game