Difference between revisions of "2D Space Shooter Part 5: Firing"

From GiderosMobile
Line 3: Line 3:
  
 
== 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 paste the following code:
+
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)
local bullet=Bullet.new(self.type,self.angle+self.ship:getRotation(),x,y)
+
Bullet.new(self.type, self.angle+self.ship:getRotation(), x, y, self.ship.isplayer)
bullet.friendly=self.ship.friendly
+
self.reload = self.rate
self.reload=self.rate
+
else -- decrement reload counter
else
+
self.reload -= 1
self.reload-=1
 
 
end
 
end
 
end
 
end
Line 32: Line 30:
 
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 the files and put them in the gfx folder.
+
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:
+
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.3
+
local scale = 0.5
 
self:setTexture(texture)
 
self:setTexture(texture)
self:setDimensions(tw*scale,th*scale)
+
self:setDimensions(tw*scale, th*scale)
self.damage=bullet_def.damage
+
self:setAnchorPoint(0.5, 0.5)
self.speed=bullet_def.speed
 
self:setAnchorPoint(0.5,0.5)
 
 
self:setRotation(angle)
 
self:setRotation(angle)
self:setPosition(x,y)
+
self:setPosition(x, y)
local ax,ay=self:getAnchorPosition()
+
-- store the bullet params
self.dx,self.dy=math.sin(^<angle),math.cos(^<angle)
+
self.damage = bullet_def.damage
BULLETS:addChild(self)
+
self.speed = bullet_def.speed
-- Add to actors list
+
self.isplayer = isplayer -- is it a player bullet (true) or an enemy bullet (nil)?
ACTORS[self]=true
+
self.dx, self.dy = math.sin(^<angle), math.cos(^<angle)
-- Add to collision world
+
-- add to sprite layer
BUMP_WORLD:add(self,x-ax,y-ay,tw*scale,th*scale)
+
BULLETS_LAYER:addChild(self)
 +
-- add to bullets list
 +
BULLETS[self] = true
 
end
 
end
  
 
function Bullet:destroy()
 
function Bullet:destroy()
-- Remove from collision world
+
BULLETS[self] = nil -- remove from bullets list
BUMP_WORLD:remove(self)
+
self:removeFromParent() -- remove from screen
-- Remove from actors list
 
ACTORS[self]=nil
 
-- Remove from screen
 
self:removeFromParent()
 
 
end
 
end
  
-- This function will check collisions between this bullet (item) and some other object (other)
+
-- the check collision function
local function collision_filter(item,other)
+
function Bullet:checkMidCollision(bullet, ship)
-- Other can be a Ship or another Bullet
+
local bulletx, bullety, bulletw, bulleth, shipx, shipy, shipw, shiph =
-- If it has a property named 'damage' then it is a Bullet, and we don't want bullets to collide with each others
+
bullet:getX(), bullet:getY(), bullet:getWidth(), bullet:getHeight(),
if other.damage then return nil end
+
ship:getX(), ship:getY(), ship:getWidth(), ship:getHeight()
-- We also want to avoid being killed by our own shots, or enemies killing each others
+
if (bulletx + bulletw/2 > shipx and -- right side from middle
if item.friendly==other.friendly then return nil end
+
bulletx < shipx + shipw/2 and
-- Now we have a Bullet colliding with an enemy ship
+
bullety + bulleth/2 > shipy and
return 'touch'
+
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)
local cols,colslen
+
for k, _ in pairs(BULLETS) do -- iterate the bullets list
x,y,cols,colslen=BUMP_WORLD:move(self,x,y,collision_filter)
+
for k1, _ in pairs(ACTORS) do -- iterate the actors list
for k,col in pairs(cols) do
+
local c, b, s = self:checkMidCollision(k, k1) -- collision, bullet, ship
col.other:hit(self.damage)
+
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 or colslen>0 then
+
-- 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
Line 106: Line 116:
 
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.
 
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 two major changes:
+
There are a couple of major changes:
* The bullet will move by itself
+
* 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.
* The collision detection
+
* we iterate through the bullets and the actors list for collision detection
Once we have computed the new bullet position, we pass it to bump engine 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.
+
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. To tell this to bump engine, we use a collision filter function. The filter function returns nil (or false) if objects don't collide. Otherwise it returns a collision type, which is, in our case 'touch'.
+
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':
 +
<syntaxhighlight lang="lua">
 +
function Ship:hit(damage)
 +
ACTORS = {}
 +
BULLETS = {}
 +
end
 +
</syntaxhighlight>
  
Before going further, let's implement the 'hit' function in the Ship class: it will decrease our armour resistance and call a future 'explode' function when appropriate:
+
And 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
Line 127: Line 146:
 
Now that our cannons and bullets are defined, we can uncomment the Cannon instancing in ships.lua, 'init' method:
 
Now that our cannons and bullets are 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>
 
</syntaxhighlight>
  

Revision as of 20:21, 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':

function Ship:hit(damage)
ACTORS = {}
BULLETS = {}
end

And 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

Now that our cannons and bullets are 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