Difference between revisions of "2D Space Shooter Part 6: Enemies"

From GiderosMobile
 
(6 intermediate revisions by the same user not shown)
Line 4: Line 4:
 
Again we will subclass our ship class for enemies.
 
Again we will subclass our ship class for enemies.
  
Create a new file named ''''enemy.lua''''. This new file will depend on the Ship Class too but this time we will use Gideros code dependency by code instead of Gideros project setting (you can do it to the player.lua file as well).
+
Create a new file named ''''enemy.lua''''. This new file will depend on the Ship Class too (we will use Gideros code dependency by code).
  
 
== Enemy class ==
 
== Enemy class ==
Line 10: Line 10:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
--!NEEDS:ships.lua
 
--!NEEDS:ships.lua
EnemyShip=Core.class(Ship)
+
EnemyShip = Core.class(Ship)
  
function EnemyShip:init(type,x,y)
+
function EnemyShip:init(type, x, y)
self.posx=x
+
self.posx = x
self.posy=y
+
self.posy = y
 
self:setRotation(180)
 
self:setRotation(180)
self.fire=true
+
self.fire = true
ENEMIES:addChild(self)
+
ENEMIES_LAYER:addChild(self)
 
end
 
end
  
 
function EnemyShip:advance(amount)
 
function EnemyShip:advance(amount)
self.posy+=amount
+
self.posy += amount
 
end
 
end
  
 
function EnemyShip:tick(delay)
 
function EnemyShip:tick(delay)
self:setPosition(self.posx,self.posy)
+
self:setPosition(self.posx, self.posy)
Ship.tick(self,delay)
+
Ship.tick(self, delay)
 
end
 
end
  
 
function EnemyShip:explode()
 
function EnemyShip:explode()
-- Here we could show some explosion animation and play a sound
+
-- here we could show some explosion animation and play a sound
 
self:destroy()
 
self:destroy()
 
end
 
end
Line 43: Line 43:
 
*the sixth character with tell how much time to wait before the next row of ship shows up, or whether the row is the last of a wave
 
*the sixth character with tell how much time to wait before the next row of ship shows up, or whether the row is the last of a wave
  
In a new file called ''''level.lua'''', we define our level manager, starting by our first (and only in this tutorial) level.
+
In a new file called '''level.lua''', we define our level manager, starting by our first (and only in this tutorial) level.
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
local LEVEL1=[[
+
local LEVEL1 = [[
 
..A..0
 
..A..0
 
.....!
 
.....!
Line 62: Line 62:
 
]]
 
]]
  
local LEVELS={ LEVEL1 }
+
local LEVELS = { LEVEL1 }
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 
So this is our 1st level data. We can almost see how ships will be shown on screen.
 
So this is our 1st level data. We can almost see how ships will be shown on screen.
  
 
Now, below LEVELS, we add our loading (init) and sequencing (tick) code:
 
Now, below LEVELS, we add our loading (init) and sequencing (tick) code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
local SCROLL_DELTA=0.02
+
local SCROLL_DELTA = 0.02
  
Level=Core.class(Object)
+
Level = Core.class(Object)
  
 
function Level:init(number)
 
function Level:init(number)
-- Will hold our level info
+
-- will hold our level info
self.levelinfo={}
+
self.levelinfo = {}
-- Will hold our position in the level
+
-- will hold our position in the level
self.levelline=1
+
self.levelline = 1
-- Hold all the enemy ships in the current wave
+
-- hold all the enemy ships in the current wave
self.enemies={}
+
self.enemy_wave = {}
-- Hold the enemy line scroll amount
+
-- hold the enemy line scroll amount
self.scroll=0
+
self.scroll = 0
-- Fill in level info
+
-- fill in level info
 
for level_line in LEVELS[number]:gmatch("[%w%p]+") do
 
for level_line in LEVELS[number]:gmatch("[%w%p]+") do
assert(#level_line==6,"Level description line is not 6 characters long.")
+
assert(#level_line == 6, "Level description line is not 6 characters long.")
local wait=tonumber(level_line:sub(6)) or 0
+
local wait = tonumber(level_line:sub(6)) or 0
local clear=level_line:sub(6)=="!"
+
local clear = level_line:sub(6) == "!" -- this is a boolean
table.insert(self.levelinfo,{ enemies=level_line:sub(1,5), wait=wait, clear=clear})
+
table.insert(self.levelinfo, { enemies=level_line:sub(1,5), wait=wait, clear=clear } )
 
end
 
end
--Register as actor
+
-- register as a wave level
ACTORS[self]=true
+
LEVELS_WAVES[self] = true
 
end
 
end
  
 
function Level:tick(delay)
 
function Level:tick(delay)
if self.scroll==0 then
+
if self.scroll == 0 then
--Not started scrolling: build an enemy row
+
-- not started scrolling: build an enemy row
local lineinfo=self.levelinfo[self.levelline]
+
local lineinfo = self.levelinfo[self.levelline]
for i=1,5 do
+
for i = 1, 5 do
local type=lineinfo.enemies:sub(i,i)
+
local type = lineinfo.enemies:sub(i,i)
if type~="." then
+
if type ~= "." then
local enemy=EnemyShip.new(type,SHIP_SIZE*i+SCR_LEFT,SCR_TOP-SHIP_SIZE/2)
+
local enemy = EnemyShip.new(type, SHIP_SIZE*i+SCR_LEFT, SCR_TOP-SHIP_SIZE/2)
self.enemies[enemy]=true
+
self.enemy_wave[enemy] = true
 
enemy:advance(SCROLL_DELTA*SHIP_SIZE)
 
enemy:advance(SCROLL_DELTA*SHIP_SIZE)
 
end
 
end
 
end
 
end
self.scroll=SCROLL_DELTA
+
self.scroll = SCROLL_DELTA
elseif self.scroll<1 then
+
elseif self.scroll < 1 then
self.scroll+=SCROLL_DELTA
+
self.scroll += SCROLL_DELTA
for k,_ in pairs(self.enemies) do
+
for k, _ in pairs(self.enemy_wave) do
 
if ACTORS[k] then
 
if ACTORS[k] then
 
k:advance(SCROLL_DELTA*SHIP_SIZE)
 
k:advance(SCROLL_DELTA*SHIP_SIZE)
Line 113: Line 114:
 
end
 
end
 
else
 
else
local lineinfo=self.levelinfo[self.levelline]
+
local lineinfo = self.levelinfo[self.levelline]
if lineinfo then --If no lineinfo, it means that we reached the end of the level
+
if lineinfo then -- if no lineinfo, it means that we reached the end of the level
if lineinfo.wait>0 then
+
if lineinfo.wait > 0 then
lineinfo.wait-=SCROLL_DELTA
+
lineinfo.wait -= SCROLL_DELTA
 
else
 
else
local enemy_count=0
+
local enemy_count = 0
 
if lineinfo.clear then
 
if lineinfo.clear then
for k,_ in pairs(self.enemies) do
+
for k, _ in pairs(self.enemy_wave) do
if ACTORS[k] then enemy_count+=1 end
+
if ACTORS[k] then enemy_count += 1 end
 
end
 
end
 
end
 
end
if enemy_count==0 then
+
if enemy_count == 0 then
self.levelline+=1
+
self.levelline += 1
 
if lineinfo.clear then
 
if lineinfo.clear then
self.enemies={}
+
self.enemy_wave = {}
 
end
 
end
 
if self.levelinfo[self.levelline] then
 
if self.levelinfo[self.levelline] then
self.scroll=0
+
self.scroll = 0
 
else
 
else
-- Level finished
+
-- level finished
 
print("FINISHED")
 
print("FINISHED")
 
end
 
end
Line 140: Line 141:
 
end
 
end
 
end
 
end
 +
</syntaxhighlight>
 +
 +
 +
We are almost done! Add the levels waves table to 'main.lua' and the ''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)
 +
LEVELS_WAVES = {} -- our levels waves of enemies
 +
 +
-- ...
 +
 +
-- 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
 +
for k, _ in pairs(LEVELS_WAVES) do
 +
k:tick(1)
 +
end
 +
end)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
== Kicking off our level ==
 
== Kicking off our level ==
It is time to see what we just did by adding a single line of code at the very bottom of the "main.lua" file, let's do it:
+
It is time to see what we just did by adding a single line of code at the very bottom of the "main.lua" file:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
Level.new(1)
 
Level.new(1)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
And the result:
+
'''And the result!'''
{{#widget:GApp|app=SpaceShooter_EN1V2.GApp|width=320|height=480|plugins=bump}}
+
 
 +
{{#widget:GApp|app=SpaceShooter_EN1V3.GApp|width=320|height=480|plugins=bump}}
 +
 
 +
 
 +
== Going further ==
 +
In this tutorial we built the foundation of a 2D Space Shooter game. There are some missing pieces but you should be able to implement them or ask on the Gideros forum, we will be happy to help.
 +
 
 +
I will end this tutorial with some ideas:
 +
* '''2D Space Shooter Part 7: Boosters'''
 +
* '''2D Space Shooter Part 8: Score'''
 +
* '''2D Space Shooter Part 9: Effects'''
 +
* '''2D Space Shooter Part 10: Going even further'''
 +
 
 +
Thank you and hope you learned a thing or two. Peace!
 +
 
  
 +
Prev.: [[2D Space Shooter Part 5: Firing]]</br>
 +
'''Next: 2D Space Shooter Part 7: Boosters'''
  
'''Prev.: [[2D Space Shooter Part 5: Firing]]'''</br>
 
'''Next: [[2D Space Shooter Part 7: Boosters]]'''
 
  
 +
'''[[Tutorial - Making a 2D space shooter game]]'''
 
{{GIDEROS IMPORTANT LINKS}}
 
{{GIDEROS IMPORTANT LINKS}}

Latest revision as of 22:56, 15 November 2023

This won't be a space shooter without enemies to shoot at. Let's define our enemies.

Again we will subclass our ship class for enemies.

Create a new file named 'enemy.lua'. This new file will depend on the Ship Class too (we will use Gideros code dependency by code).

Enemy class

Enemies will come from the top of the screen, so ships will head toward the bottom.

--!NEEDS:ships.lua
EnemyShip = Core.class(Ship)

function EnemyShip:init(type, x, y)
	self.posx = x
	self.posy = y
	self:setRotation(180)
	self.fire = true
	ENEMIES_LAYER:addChild(self)
end

function EnemyShip:advance(amount)
	self.posy += amount
end

function EnemyShip:tick(delay)
	self:setPosition(self.posx, self.posy)
	Ship.tick(self, delay)
end

function EnemyShip:explode()
	-- here we could show some explosion animation and play a sound
	self:destroy()
end

Level Management

Now we need to make the enemies appear in waves. This part is a bit trickier, as it involves choosing a format to design our levels.

I decided to use a plain text representation:

  • enemies will be placed in up to five positions on each row, possibly spanning multiple rows
  • each line of our level definition will be six character long: one character for each 6 possible position, indicating if a ship will be spawn at that location, which kind of ship, plus a sixth character to control the ship waves
  • the sixth character with tell how much time to wait before the next row of ship shows up, or whether the row is the last of a wave

In a new file called level.lua, we define our level manager, starting by our first (and only in this tutorial) level.

local LEVEL1 = [[
..A..0
.....!
.A.A.0
.....!
..A..1
.A...1
...A.0
.....!
..A..0
.B.B.0
.....!
A...A0
..C..0
.....!
]]

local LEVELS = { LEVEL1 }

So this is our 1st level data. We can almost see how ships will be shown on screen.

Now, below LEVELS, we add our loading (init) and sequencing (tick) code:

local SCROLL_DELTA = 0.02

Level = Core.class(Object)

function Level:init(number)
	-- will hold our level info
	self.levelinfo = {}
	-- will hold our position in the level
	self.levelline = 1
	-- hold all the enemy ships in the current wave
	self.enemy_wave = {}
	-- hold the enemy line scroll amount
	self.scroll = 0
	-- fill in level info
	for level_line in LEVELS[number]:gmatch("[%w%p]+") do
		assert(#level_line == 6, "Level description line is not 6 characters long.")
		local wait = tonumber(level_line:sub(6)) or 0
		local clear = level_line:sub(6) == "!" -- this is a boolean
		table.insert(self.levelinfo, { enemies=level_line:sub(1,5), wait=wait, clear=clear } )
	end
	-- register as a wave level
	LEVELS_WAVES[self] = true
end

function Level:tick(delay)
	if self.scroll == 0 then
		-- not started scrolling: build an enemy row
		local lineinfo = self.levelinfo[self.levelline]
		for i = 1, 5 do
			local type = lineinfo.enemies:sub(i,i)
			if type ~= "." then
				local enemy = EnemyShip.new(type, SHIP_SIZE*i+SCR_LEFT, SCR_TOP-SHIP_SIZE/2)
				self.enemy_wave[enemy] = true
				enemy:advance(SCROLL_DELTA*SHIP_SIZE)
			end
		end
		self.scroll = SCROLL_DELTA
	elseif self.scroll < 1 then
		self.scroll += SCROLL_DELTA
		for k, _ in pairs(self.enemy_wave) do
			if ACTORS[k] then
				k:advance(SCROLL_DELTA*SHIP_SIZE)
			end
		end
	else
		local lineinfo = self.levelinfo[self.levelline]
		if lineinfo then -- if no lineinfo, it means that we reached the end of the level
			if lineinfo.wait > 0 then
				lineinfo.wait -= SCROLL_DELTA
			else
				local enemy_count = 0
				if lineinfo.clear then
					for k, _ in pairs(self.enemy_wave) do
						if ACTORS[k] then enemy_count += 1 end
					end
				end
				if enemy_count == 0 then
					self.levelline += 1
					if lineinfo.clear then
						self.enemy_wave = {}
					end
					if self.levelinfo[self.levelline] then
						self.scroll = 0
					else
						-- level finished
						print("FINISHED")
					end
				end
			end
		end
	end
end


We are almost done! Add the levels waves table to 'main.lua' and the 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)
LEVELS_WAVES = {} -- our levels waves of enemies

-- ...

-- 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
	for k, _ in pairs(LEVELS_WAVES) do
		k:tick(1)
	end
end)

Kicking off our level

It is time to see what we just did by adding a single line of code at the very bottom of the "main.lua" file:

Level.new(1)

And the result!


Going further

In this tutorial we built the foundation of a 2D Space Shooter game. There are some missing pieces but you should be able to implement them or ask on the Gideros forum, we will be happy to help.

I will end this tutorial with some ideas:

  • 2D Space Shooter Part 7: Boosters
  • 2D Space Shooter Part 8: Score
  • 2D Space Shooter Part 9: Effects
  • 2D Space Shooter Part 10: Going even further

Thank you and hope you learned a thing or two. Peace!


Prev.: 2D Space Shooter Part 5: Firing
Next: 2D Space Shooter Part 7: Boosters


Tutorial - Making a 2D space shooter game