Difference between revisions of "2D Space Shooter Part 5: Firing"
m (Text replacement - "<source" to "<syntaxhighlight") |
|||
(9 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
+ | __TOC__ | ||
Last chapter was quick, this one will be longer. We will now deal with weapons! | Last chapter was quick, this one will be longer. We will now deal with weapons! | ||
== Cannons == | == Cannons == | ||
− | Remember how we defined cannons in our Ship class ? It is now time to implement them. A cannon is basically something that will throw bullets repeatedly. Create a new file named 'cannon.lua' and | + | Remember how we defined cannons in our Ship class? It is now time to implement them. A cannon is basically something that will throw bullets repeatedly. Create a new file named '''cannon.lua''' and copy the following code: |
− | |||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | Cannon=Core.class(Object) | + | Cannon = Core.class(Object) |
− | function Cannon:init(def,scale,ship) | + | function Cannon:init(def, scale, ship) |
− | self.x=def.x*scale | + | self.x = def.x*scale |
− | self.y=def.y*scale | + | self.y = def.y*scale |
− | self.type=def.type | + | self.type = def.type |
− | self.rate=def.rate | + | self.rate = def.rate |
− | self.angle=def.angle or 0 | + | self.angle = def.angle or 0 |
− | self.ship=ship | + | self.ship = ship |
− | self.reload=0 | + | self.reload = 0 |
end | end | ||
function Cannon:fire() | function Cannon:fire() | ||
− | if self.reload==0 then | + | if self.reload == 0 then -- ready to fire |
− | local x,y=self.ship:localToGlobal(self.x,self.y) | + | local x, y = self.ship:localToGlobal(self.x, self.y) |
− | + | Bullet.new(self.type, self.angle+self.ship:getRotation(), x, y, self.ship.isplayer) | |
− | + | self.reload = self.rate | |
− | self.reload=self.rate | + | else -- decrement reload counter |
− | else | + | self.reload -= 1 |
− | self.reload-=1 | ||
end | end | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
The Cannon class isn't a Sprite, but a basic Object. In the 'init' method, we mostly copy the cannon definition locally and initialize our reload counter that will be used to count time between each shot. We also compute the ship-relative position of the cannon. | The Cannon class isn't a Sprite, but a basic Object. In the 'init' method, we mostly copy the cannon definition locally and initialize our reload counter that will be used to count time between each shot. We also compute the ship-relative position of the cannon. | ||
− | In the 'fire' method, we decrement the reload counter until it reaches 0. When at 0, we create a Bullet and reset our reload counter to the cannon rate value. | + | In the 'fire' method, we decrement the reload counter until it reaches 0. When at 0, we create a Bullet and reset our reload counter to the cannon rate value. The bullet will know if it was fired by the player or by an enemy ship thanks to the ''self.ship.isplayer'' boolean flag. |
== Bullets == | == Bullets == | ||
− | + | Please grab the '''[[Media:2D Spaceshooter Laser.zip|weapons graphics]]''', unzip it and put the images in the gfx folder. | |
+ | The Bullet class needs more work: it will be a graphic object (inherited from Pixel), but will also have to check collisions. Here is the Bullet code, to copy into a '''bullet.lua''' file: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | local BULLETS_DEF={ | + | local BULLETS_DEF = { |
− | laser={ file="laser.png", speed=5, damage=1 }, | + | laser = { file="laser.png", speed=5, damage=1 }, |
− | missile={ file="rocket.png", speed=1, damage=10 }, | + | missile = { file="rocket.png", speed=1, damage=10 }, |
} | } | ||
− | Bullet=Core.class(Pixel,function (type) end) | + | Bullet = Core.class(Pixel, function(type) end) |
− | function Bullet:init(type,angle,x,y) | + | function Bullet:init(type, angle, x, y, isplayer) |
− | local bullet_def=BULLETS_DEF[type] | + | local bullet_def = BULLETS_DEF[type] |
− | assert(bullet_def,"No such bullet type: "..type) | + | assert(bullet_def, "No such bullet type: "..type) |
− | local texture=Texture.new("gfx/"..bullet_def.file,true) | + | local texture = Texture.new("gfx/"..bullet_def.file, true) |
− | local tw,th=texture:getWidth(),texture:getHeight() | + | local tw, th = texture:getWidth(), texture:getHeight() |
− | local scale=0. | + | local scale = 0.5 |
self:setTexture(texture) | self:setTexture(texture) | ||
− | self:setDimensions(tw*scale,th*scale) | + | self:setDimensions(tw*scale, th*scale) |
− | + | self:setAnchorPoint(0.5, 0.5) | |
− | |||
− | self:setAnchorPoint(0.5,0.5) | ||
self:setRotation(angle) | self:setRotation(angle) | ||
− | self:setPosition(x,y) | + | self:setPosition(x, y) |
− | + | -- store the bullet params | |
− | self.dx,self.dy=math.sin(^<angle),math.cos(^<angle) | + | self.damage = bullet_def.damage |
− | + | self.speed = bullet_def.speed | |
− | -- | + | self.isplayer = isplayer -- is it a player bullet (true) or an enemy bullet (nil)? |
− | + | self.dx, self.dy = math.sin(^<angle), math.cos(^<angle) | |
− | + | -- add to sprite layer | |
− | + | BULLETS_LAYER:addChild(self) | |
+ | -- add to bullets list | ||
+ | BULLETS[self] = true | ||
end | end | ||
function Bullet:destroy() | function Bullet:destroy() | ||
− | + | BULLETS[self] = nil -- remove from bullets list | |
− | + | self:removeFromParent() -- remove from screen | |
− | |||
− | |||
− | |||
− | self:removeFromParent() | ||
end | end | ||
− | -- | + | -- the check collision function |
− | + | function Bullet:checkMidCollision(bullet, ship) | |
− | -- | + | local bulletx, bullety, bulletw, bulleth, shipx, shipy, shipw, shiph = |
− | + | bullet:getX(), bullet:getY(), bullet:getWidth(), bullet:getHeight(), | |
− | + | ship:getX(), ship:getY(), ship:getWidth(), ship:getHeight() | |
− | + | if (bulletx + bulletw/2 > shipx and -- right side from middle | |
− | + | bulletx < shipx + shipw/2 and | |
− | -- | + | bullety + bulleth/2 > shipy and |
− | + | bullety < shipy + shiph/2) or | |
+ | (bulletx - bulletw/2 < shipx and -- left side from middle | ||
+ | bulletx > shipx - shipw/2 and | ||
+ | bullety - bulleth/2 < shipy and | ||
+ | bullety > shipy - shiph/2) then | ||
+ | if (bullet.isplayer and not ship.isplayer) or -- player bullet vs enemy ship | ||
+ | (not bullet.isplayer and ship.isplayer) then -- enemy bullet vs player ship | ||
+ | return true, bullet, ship -- collision | ||
+ | end | ||
+ | else | ||
+ | return false -- no collision | ||
+ | end | ||
end | end | ||
function Bullet:tick(delay) | function Bullet:tick(delay) | ||
− | local x,y=self:getPosition() | + | local x, y = self:getPosition() |
− | x+=self.dx*self.speed | + | x += self.dx*self.speed |
− | y-=self.dy*self.speed | + | y -= self.dy*self.speed |
− | self:setPosition(x,y) | + | self:setPosition(x, y) |
− | + | for k, _ in pairs(BULLETS) do -- iterate the bullets list | |
− | + | for k1, _ in pairs(ACTORS) do -- iterate the actors list | |
− | + | local c, b, s = self:checkMidCollision(k, k1) -- collision, bullet, ship | |
− | + | if c then -- if collision | |
+ | b:destroy() -- destroy bullet | ||
+ | s:hit(self.damage) -- damage ship | ||
+ | end | ||
+ | end | ||
end | end | ||
− | if x<SCR_LEFT or x>SCR_RIGHT or y<SCR_TOP or y>SCR_BOTTOM | + | -- destroy the bullet when it goes out of screen |
+ | if x<SCR_LEFT or x>SCR_RIGHT or y<SCR_TOP or y>SCR_BOTTOM then | ||
self:destroy() | self:destroy() | ||
end | end | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
− | We borrowed a lot of code from the Ship class: the setting up of the sprite itself | + | We borrowed a lot of code from the Ship class: the setting up of the sprite itself, the bullet is being added and removed from collision world and actors in the same way. |
− | There are | + | There are a couple of major changes: |
− | * | + | * the bullet will move by itself |
− | For that we precomputed its movement direction from its angle with a little bit of help from trigonometry (sin/cos) in 'Bullet:init', and we use that direction (self.dx,self.dy) and the bullet speed in 'Bullet:tick' to increment the bullet position. | + | For that we precomputed its movement direction from its angle with a little bit of help from trigonometry (sin/cos) in 'Bullet:init', and we use that direction (self.dx, self.dy) and the bullet speed in 'Bullet:tick' to increment the bullet position. |
− | * | + | * we iterate through the bullets and the actors list for collision detection |
− | Once we have computed the new bullet position, we pass it to | + | Once we have computed the new bullet position, we pass it to a collision function and check for eventual collisions. If something is hit, we call its 'hit' method and give it the amount of damage the bullet should cause. |
+ | * we destroy the bullets which go out of screen | ||
− | We don't want the bullet to collide with other bullets, and we don't want either a bullet to hit the ship type that fired it | + | We don't want the bullet to collide with other bullets, and we don't want either a bullet to hit the ship type that fired it, that's why we 'marked' the bullet as being a player bullet or not. |
− | |||
+ | Before going further, add the bullets list to 'main.lua' plus the bullets 'tick' function in the game loop: | ||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- lists of all objects that should receive frame ticks | ||
+ | ACTORS = {} -- the ships table | ||
+ | BULLETS = {} -- the bullet table (both player bullets and enemy bullets) | ||
+ | |||
+ | -- ... | ||
+ | |||
+ | -- this is our game loop | ||
+ | stage:addEventListener(Event.ENTER_FRAME, function() | ||
+ | background:advance(1) | ||
+ | for k, _ in pairs(ACTORS) do | ||
+ | k:tick(1) | ||
+ | end | ||
+ | for k, _ in pairs(BULLETS) do | ||
+ | k:tick(1) | ||
+ | end | ||
+ | end) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Now we implement the 'hit' function in the Ship class above the tick function. This will decrease our armour resistance and call a future 'explode' function when appropriate. | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
function Ship:hit(damage) | function Ship:hit(damage) | ||
− | self.armour-=damage | + | self.armour -= damage |
− | if self.armour<0 then | + | if self.armour < 0 then |
− | self:explode() | + | self:explode() |
end | end | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
== Testing == | == Testing == | ||
− | + | Our cannons and bullets are now defined, we can uncomment the Cannon instancing in ships.lua, 'init' method: | |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | self.cannons[k]=Cannon.new(cdef,scale,self) | + | self.cannons[k] = Cannon.new(cdef, scale, self) |
− | </ | + | </syntaxhighlight> |
+ | |||
+ | The ship will fire when you hold the mouse button down. | ||
+ | |||
+ | {{#widget:GApp|app=SpaceShooter_FR1V2.GApp|width=320|height=480|plugins=bump}} | ||
− | |||
− | + | Prev.: [[2D Space Shooter Part 4: Player]]</br> | |
+ | '''Next: [[2D Space Shooter Part 6: Enemies]]''' | ||
− | |||
− | [[2D | + | '''[[Tutorial - Making a 2D space shooter game]]''' |
+ | {{GIDEROS IMPORTANT LINKS}} |
Latest revision as of 20:10, 15 November 2023
Last chapter was quick, this one will be longer. We will now deal with weapons!
Cannons
Remember how we defined cannons in our Ship class? It is now time to implement them. A cannon is basically something that will throw bullets repeatedly. Create a new file named cannon.lua and copy the following code:
Cannon = Core.class(Object)
function Cannon:init(def, scale, ship)
self.x = def.x*scale
self.y = def.y*scale
self.type = def.type
self.rate = def.rate
self.angle = def.angle or 0
self.ship = ship
self.reload = 0
end
function Cannon:fire()
if self.reload == 0 then -- ready to fire
local x, y = self.ship:localToGlobal(self.x, self.y)
Bullet.new(self.type, self.angle+self.ship:getRotation(), x, y, self.ship.isplayer)
self.reload = self.rate
else -- decrement reload counter
self.reload -= 1
end
end
The Cannon class isn't a Sprite, but a basic Object. In the 'init' method, we mostly copy the cannon definition locally and initialize our reload counter that will be used to count time between each shot. We also compute the ship-relative position of the cannon.
In the 'fire' method, we decrement the reload counter until it reaches 0. When at 0, we create a Bullet and reset our reload counter to the cannon rate value. The bullet will know if it was fired by the player or by an enemy ship thanks to the self.ship.isplayer boolean flag.
Bullets
Please grab the weapons graphics, unzip it and put the images in the gfx folder.
The Bullet class needs more work: it will be a graphic object (inherited from Pixel), but will also have to check collisions. Here is the Bullet code, to copy into a bullet.lua file:
local BULLETS_DEF = {
laser = { file="laser.png", speed=5, damage=1 },
missile = { file="rocket.png", speed=1, damage=10 },
}
Bullet = Core.class(Pixel, function(type) end)
function Bullet:init(type, angle, x, y, isplayer)
local bullet_def = BULLETS_DEF[type]
assert(bullet_def, "No such bullet type: "..type)
local texture = Texture.new("gfx/"..bullet_def.file, true)
local tw, th = texture:getWidth(), texture:getHeight()
local scale = 0.5
self:setTexture(texture)
self:setDimensions(tw*scale, th*scale)
self:setAnchorPoint(0.5, 0.5)
self:setRotation(angle)
self:setPosition(x, y)
-- store the bullet params
self.damage = bullet_def.damage
self.speed = bullet_def.speed
self.isplayer = isplayer -- is it a player bullet (true) or an enemy bullet (nil)?
self.dx, self.dy = math.sin(^<angle), math.cos(^<angle)
-- add to sprite layer
BULLETS_LAYER:addChild(self)
-- add to bullets list
BULLETS[self] = true
end
function Bullet:destroy()
BULLETS[self] = nil -- remove from bullets list
self:removeFromParent() -- remove from screen
end
-- the check collision function
function Bullet:checkMidCollision(bullet, ship)
local bulletx, bullety, bulletw, bulleth, shipx, shipy, shipw, shiph =
bullet:getX(), bullet:getY(), bullet:getWidth(), bullet:getHeight(),
ship:getX(), ship:getY(), ship:getWidth(), ship:getHeight()
if (bulletx + bulletw/2 > shipx and -- right side from middle
bulletx < shipx + shipw/2 and
bullety + bulleth/2 > shipy and
bullety < shipy + shiph/2) or
(bulletx - bulletw/2 < shipx and -- left side from middle
bulletx > shipx - shipw/2 and
bullety - bulleth/2 < shipy and
bullety > shipy - shiph/2) then
if (bullet.isplayer and not ship.isplayer) or -- player bullet vs enemy ship
(not bullet.isplayer and ship.isplayer) then -- enemy bullet vs player ship
return true, bullet, ship -- collision
end
else
return false -- no collision
end
end
function Bullet:tick(delay)
local x, y = self:getPosition()
x += self.dx*self.speed
y -= self.dy*self.speed
self:setPosition(x, y)
for k, _ in pairs(BULLETS) do -- iterate the bullets list
for k1, _ in pairs(ACTORS) do -- iterate the actors list
local c, b, s = self:checkMidCollision(k, k1) -- collision, bullet, ship
if c then -- if collision
b:destroy() -- destroy bullet
s:hit(self.damage) -- damage ship
end
end
end
-- destroy the bullet when it goes out of screen
if x<SCR_LEFT or x>SCR_RIGHT or y<SCR_TOP or y>SCR_BOTTOM then
self:destroy()
end
end
We borrowed a lot of code from the Ship class: the setting up of the sprite itself, the bullet is being added and removed from collision world and actors in the same way.
There are a couple of major changes:
- the bullet will move by itself
For that we precomputed its movement direction from its angle with a little bit of help from trigonometry (sin/cos) in 'Bullet:init', and we use that direction (self.dx, self.dy) and the bullet speed in 'Bullet:tick' to increment the bullet position.
- we iterate through the bullets and the actors list for collision detection
Once we have computed the new bullet position, we pass it to a collision function and check for eventual collisions. If something is hit, we call its 'hit' method and give it the amount of damage the bullet should cause.
- we destroy the bullets which go out of screen
We don't want the bullet to collide with other bullets, and we don't want either a bullet to hit the ship type that fired it, that's why we 'marked' the bullet as being a player bullet or not.
Before going further, add the bullets list to 'main.lua' plus the bullets 'tick' function in the game loop:
-- lists of all objects that should receive frame ticks
ACTORS = {} -- the ships table
BULLETS = {} -- the bullet table (both player bullets and enemy bullets)
-- ...
-- this is our game loop
stage:addEventListener(Event.ENTER_FRAME, function()
background:advance(1)
for k, _ in pairs(ACTORS) do
k:tick(1)
end
for k, _ in pairs(BULLETS) do
k:tick(1)
end
end)
Now we implement the 'hit' function in the Ship class above the tick function. This will decrease our armour resistance and call a future 'explode' function when appropriate.
function Ship:hit(damage)
self.armour -= damage
if self.armour < 0 then
self:explode()
end
end
Testing
Our cannons and bullets are now defined, we can uncomment the Cannon instancing in ships.lua, 'init' method:
self.cannons[k] = Cannon.new(cdef, scale, self)
The ship will fire when you hold the mouse button down.
Prev.: 2D Space Shooter Part 4: Player
Next: 2D Space Shooter Part 6: Enemies
Tutorial - Making a 2D space shooter game