Difference between revisions of "Tuto tiny-ecs beatemup Part 4 LevelX"

From GiderosMobile
(wip)
 
(10 intermediate revisions by the same user not shown)
Line 6: Line 6:
 
The LevelX scene holds the game loop and controls the flow of each levels.
 
The LevelX scene holds the game loop and controls the flow of each levels.
  
When the scene loads it constructs the level which is organised into layers, more on that in the code comments below ;-)
+
When the scene loads, it constructs the level which is organised into layers, more on that in the code comments below ;-)
  
The code is not that long given it takes care of all 3 levels of the game: level 1, 2 and 3. To make it easy to follow and debug, I use '''FIGlet''' (https://en.wikipedia.org/wiki/FIGlet).
+
The code is not that long given it takes care of all levels of the game: level 1, 2 and 3. To make it easy to follow and debug, I use '''FIGlet''' (https://en.wikipedia.org/wiki/FIGlet).
  
 
  '''I use this one https://sourceforge.net/projects/figletgenerator/'''
 
  '''I use this one https://sourceforge.net/projects/figletgenerator/'''
Line 429: Line 429:
  
 
=== LevelX:init() ===
 
=== LevelX:init() ===
I commented out the "move cursor out of the way" code because it was more annoying than anything else! You can use it if you want.
+
I commented out the "move cursor" code because it was more annoying than anything else! You can use it if you want.
  
 
==== plugins ====
 
==== plugins ====
Line 444: Line 444:
  
 
==== levels ====
 
==== levels ====
Time to build our levels. The sprite list is a table of all the actors we can interact with (the player, the nmes, collectibles, ...). We put them in a list so we can use that list to sort them in the game (this will be an ECS System).
+
Time to build our levels. The sprite list is a table of all the actors we can interact with (the player, the nmes, collectibles, ...). We put them in a list so we can use that list in the systems we will create (sAI, sAnimation, sCollectible, sCollision, ...).
  
 
''tiny.numberofnmes'' and ''tiny.numberofdestructibleobjects'' are variables we can tune to add a certain amount of enemies and destructible objects in each level.
 
''tiny.numberofnmes'' and ''tiny.numberofdestructibleobjects'' are variables we can tune to add a certain amount of enemies and destructible objects in each level.
  
The ''mapdef'' is a table which has the map dimensions, so we can pass it to functions that will require the size of the map for calculations.
+
The ''mapdef'' is a table which has the map definition (dimensions), so we can pass it to functions that will require the size of the map for calculations.
  
 
We use a camera in our game and ''camfollowoffsety'' will offset the camera following the player. It is easier to change it being a variable.
 
We use a camera in our game and ''camfollowoffsety'' will offset the camera following the player. It is easier to change it being a variable.
  
The ''buildLevel'' function is where we will build the current level the player is playing. We pass it some variables so it can do its thing that we will see below.
+
The ''buildLevel'' function is where we build the current level the player is playing. We pass it some variables so it can do its thing. Some code comments on that function below.
  
Then for the current level the player is in, we set the variables for that level: the camera offset and the number of destructible objects (I should have picked a better name!). The destructible objects spawn bonuses (collectibles) when they are destroyed (extended life, jumps).
+
Then we set the variables for each level: the camera offset and the number of destructible objects. The destructible objects spawn collectible when they are destroyed (extended life, jumps).
  
 
We randomly place the destructible objects throughout the level using the map definition. We place them between 25% and 90% of the map length and a little bit above the bottom of the map.
 
We randomly place the destructible objects throughout the level using the map definition. We place them between 25% and 90% of the map length and a little bit above the bottom of the map.
Line 461: Line 461:
  
 
==== player1 ====
 
==== player1 ====
After we have added the graphics for our level, we add the player1.
+
The player is an ECS entity we will create in the next chapters. The arguments to the init function are: the layer the player sprite will be added to, the position as a vector, and the layer for any fancy graphics effects we may add to the player.
 
 
The player1 is an ECS entity we will create in the next chapters. The arguments the entity takes are: the layer the player1 sprite will be added to, the position as a vector, and the layer for any fancy graphics effects we may add to the player1.
 
  
 
After the entity is created, we add it to both the '''tiny-ECS''' world and the '''Bump''' world.
 
After the entity is created, we add it to both the '''tiny-ECS''' world and the '''Bump''' world.
  
 
==== hud ====
 
==== hud ====
I added a simple head up display to the game, so we can see the player1 current health, number of lifes and the number of attacking jumps available.
+
I added a simple head up display to the game, so we can see the player current health, number of lives and number of attacking jumps available.
  
XXX
+
The same way we attached ''tiny.tworld'' to the ''self.tiny'' variable, we attach some more variables to it. This makes it easier to access those variables.
  
== the menu.lua file ==
+
==== the camera ====
The '''menu.lua''' file is the same as the one in Gideros Game Template1. We add some buttons to our menu you can navigate with the mouse and the keyboard.
+
We use a slightly modified version of '''MultiPain''''s (aka rrraptor on Gideros forum) '''GCam''' Class for our camera.
  
The code:
+
Please grab it here '''[[Media:gcam_beu.lua]]''' '''(tip: right click and save link as)''' and put it in the "'''classes'''" folder.
 +
 
 +
We pass the ''mainlayer'' as the content, then using the map definition we set the camera bounds. We also set the soft and the dead size parameters and we tell it to follow the player.
 +
 
 +
'''if you are curious I added an extra updateXOnly function ;-)'''
 +
 
 +
==== order ====
 +
This is the order of the layers, from bottom to top. We first add the background layer and the other layers on top of it.
 +
 
 +
==== systems ====
 +
Once all entities are done, we add the ECS systems. We will see those ECS systems in the coming parts of the tutorial.
 +
 
 +
==== let's go ====
 +
Finally we are ready to run the game loop!
 +
 
 +
We also listen to some key events to pause the game, go fullscreen, ...
 +
 
 +
== LevelX:buildLevel ==
 +
Let's have a look at how we construct our levels.
 +
 
 +
=== background and foreground ===
 +
We draw the background and foreground for each level using the ''DrawLevelsTiled'' Class. Each level will have its own graphics. A typical level is 3*1024 pixels wide. Using a power of two size for the graphics, we anticipate any kind of optimisations we may need, plus adding the possibility to port the game to mobile!
 +
 
 +
The ''DrawLevelsTiled'' Class already tries to bring in some optimisations! You can create a file called "drawlevelstiled.lua" and put it in the '''gfx\levels\''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 +
DrawLevelsTiled = Core.class(Sprite)
 +
 +
function DrawLevelsTiled:init(xlayer, xtexpaths, xposy)
 +
-- tilemaps textures
 +
local textures = {}
 +
for i = 1, #xtexpaths do
 +
-- textures[i] = Texture.new(xtexpaths[i])
 +
-- textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.YA8}) -- best win32 perfs but b&w!
 +
textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA4444}) -- better win32 perfs but !
 +
-- textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA5551}) -- better win32 perfs but !
 +
end
 +
-- map size
 +
local tilesizetarget = 64
 +
local tilesetcols, tilesetrows = textures[1]:getWidth()/tilesizetarget, textures[1]:getHeight()/tilesizetarget
 +
-- create the tilemaps
 +
local function createTilemap(xtex)
 +
local tm = TileMap.new(
 +
tilesetcols, tilesetrows, -- map size in tiles
 +
xtex, -- tileset texture
 +
tilesizetarget, tilesizetarget -- tile size in pixel
 +
)
 +
-- build the map
 +
for i=1,tilesetcols do
 +
for j=1,tilesetrows do
 +
tm:setTile(i, j, i, j)
 +
end
 +
end
 +
return tm
 +
end
 +
-- the maps
 +
for i = 1, #textures do
 +
local map = createTilemap(textures[i])
 +
map:setPosition(map:getWidth()*(i-1), xposy)
 +
-- self:addChild(map)
 +
xlayer:addChild(map)
 +
end
 +
-- params
 +
-- self.mapwidth = self:getWidth()
 +
-- self.mapheight = self:getHeight()
 +
self.mapwidth = xlayer:getWidth()
 +
self.mapheight = xlayer:getHeight()
 +
-- clean
 +
textures = {}
 +
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== main.lua Code comments ==
+
As the name tries to suggest (DrawLevelsTiled), we take the paths to the graphics and create tiles as if we were drawing using a tilemap. The ''init'' function parameters are: the layer we add the sprites to, the paths to the textures as a table and an y offset for flexibility.
The only thing I could say is calling the ''self:updateButtons()'' function asynchronously so we let the menu fully load before calling an update on the buttons.
+
 
 +
The first step is to create the textures and put them in a table called ''textures''. When we iterate through the ''xtexpaths'' table, I experimented with some texture formats to see if I could gain some speed.
 +
 
 +
Once the textures are created and stored in a table, we can create a '''[[TileMap]]''' of 64*64px tiles.
 +
 
 +
Finally we return the size of the map and we clean the ''textures'' table just in case.
 +
 
 +
Back to ''LevelX'' Class, now that our background and foreground layers are drawn, we set the map definition. The map definition sets the boundaries of the map where the actors can freely roam.
 +
 
 +
=== the enemies ===
 +
Each level will have different kind of enemies. The first level should be easy with few enemies you can easily kill. The other levels will be increasingly difficult with more enemies a little bit harder to defeat.
 +
 
 +
So here, depending on the level we create a bunch of enemies. Enemies are ECS entities we will create in the following chapters. The game has four types of enemies of varying strength. We add them to both the '''tiny-ECS''' world and the '''Bump''' world.
 +
 
 +
=== extra gfx ===
 +
Depending on the level, we may have extra sprites we want to add to the level.
 +
 
 +
We finish the function with some house cleaning.
 +
 
 +
== LevelX:onEnterFrame '''THE GAME LOOP''' ==
 +
Before the game loop, I declare three local variables: ''leveltimer'' is the time it takes to transition to the next level, ''endleveltimer'' is the timer itself, ''extragfxx'' is to move the extra sprite we may have on the x axis.
 +
 
 +
When we defeat all enemies, the timer decreases. When it reaches 0, the current level number is increased and we load it.
 +
 
 +
When the player is jumping we update the '''camera''' only on the x axis, otherwise we update the camera on both x and y axis.
 +
 
 +
We update the '''tiny-ecs world''', so it can run all the systems (animations, movements, AI, ...). Some systems will be called only once, others every frame per second.
 +
 
 +
== LevelX:myKeysPressed ==
 +
The last function of the LevelX Class is ''myKeysPressed''.
 +
 
 +
Here we simply listen to some '''KEY_DOWN Events''', that is keys pressed on the keyboard. The key ''ESC'' will go back to the menu, the letter ''P'' will pause the game and when you press ''ALT+ENTER'' you switch the game to fullscreen.
  
 
== Next? ==
 
== Next? ==
We quickly went through what is already in the '''Gideros Game Template1''' tutorial. The next part is really where this beat'em up tutorial begins: the '''LevelX''' scene...
+
That was quite a lot of work but we coded the heart of our game and we are already nearly done!!
 +
 
 +
All is left to do is add the actors. Actors will be '''ECS''' entites, those entities will have components and systems will control them.
 +
 
 +
In the next part we deal with our player1 entity.
  
  
 
Prev.: [[Tuto tiny-ecs beatemup Part 3 transitions menu options]]</br>
 
Prev.: [[Tuto tiny-ecs beatemup Part 3 transitions menu options]]</br>
'''Next: [[Tuto tiny-ecs beatemup Part 5 XXX]]'''
+
'''Next: [[Tuto tiny-ecs beatemup Part 5 ePlayer1]]'''
  
  
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
{{GIDEROS IMPORTANT LINKS}}
 
{{GIDEROS IMPORTANT LINKS}}

Latest revision as of 16:52, 18 November 2024

the levelX.lua file

This is where all the fun begins!

The LevelX scene holds the game loop and controls the flow of each levels.

When the scene loads, it constructs the level which is organised into layers, more on that in the code comments below ;-)

The code is not that long given it takes care of all levels of the game: level 1, 2 and 3. To make it easy to follow and debug, I use FIGlet (https://en.wikipedia.org/wiki/FIGlet).

I use this one https://sourceforge.net/projects/figletgenerator/

The LevelX code:

--!strict

LevelX = Core.class(Sprite)

local random = math.random
local ispaused : boolean = false

function LevelX:init()
	-- bg
	application:setBackgroundColor(g_ui_theme.backgroundcolor)
	-- move the cursor out of the way
--	if not application:isPlayerMode() then
--		local sw, sh = application:get("screenSize") -- the user's screen size!
--		application:set("cursorPosition", sw, sh) -- 0, 0
--	end
	-- _____  _     _    _  _____ _____ _   _  _____ 
	--|  __ \| |   | |  | |/ ____|_   _| \ | |/ ____|
	--| |__) | |   | |  | | |  __  | | |  \| | (___  
	--|  ___/| |   | |  | | | |_ | | | | . ` |\___ \ 
	--| |    | |___| |__| | |__| |_| |_| |\  |____) |
	--|_|    |______\____/ \_____|_____|_| \_|_____/ 
	-- tiny-ecs
	if not self.tiny then self.tiny = require "classes/tiny-ecs" end
	self.tiny.tworld = self.tiny.world()
	-- cbump (cworld)
	local bump = require "cbump"
	local bworld = bump.newWorld()
	-- _           __     ________ _____   _____ 
	--| |        /\\ \   / /  ____|  __ \ / ____|
	--| |       /  \\ \_/ /| |__  | |__) | (___  
	--| |      / /\ \\   / |  __| |  _  / \___ \ 
	--| |____ / ____ \| |  | |____| | \ \ ____) |
	--|______/_/    \_\_|  |______|_|  \_\_____/ 
	local mainlayer = Sprite.new() -- one Sprite to hold them all
	local bglayer = Sprite.new() -- bg layer
	local bgfxlayer = Sprite.new() -- bg fx layer
	local actorslayer = Sprite.new() -- actors layer
	local fgfxlayer = Sprite.new() -- fg fx layer
	local fglayer = Sprite.new() -- fg layer
	local player1inputlayer = Sprite.new() -- player1 input layer
	-- _      ________      ________ _       _____ 
	--| |    |  ____\ \    / /  ____| |     / ____|
	--| |    | |__   \ \  / /| |__  | |    | (___  
	--| |    |  __|   \ \/ / |  __| | |     \___ \ 
	--| |____| |____   \  /  | |____| |____ ____) |
	--|______|______|   \/   |______|______|_____/ 
	-- levels setup
	self.tiny.spriteslist = {} -- the actors
	self.tiny.numberofnmes = 0 -- some enemies
	self.tiny.numberofdestructibleobjects = 0 -- some destructible objects
	local mapdef = {} -- actors walking area (rect: top, left, right, bottom)
	local camfollowoffsety = 0 -- camera follow player1 y offset
	-- build level
	self:buildLevel(bglayer, fglayer, mapdef, actorslayer, bgfxlayer, bworld)
	if g_currlevel == 1 then
		camfollowoffsety = -1.7*32
		self.tiny.numberofdestructibleobjects = 8
	elseif g_currlevel == 2 then
		camfollowoffsety = -2.5*32
		self.tiny.numberofdestructibleobjects = 7
	elseif g_currlevel == 3 then
		camfollowoffsety = -2.5*32
		self.tiny.numberofdestructibleobjects = 6
	end
	-- add levels destructible objects if any
	local el
	for i = 1, self.tiny.numberofdestructibleobjects do
		-- EDestructibleObject:init(xspritelayer, xpos)
		el = EDestructibleObject.new(actorslayer, vector(
			random((mapdef.r-mapdef.l)*0.25, (mapdef.r-mapdef.l)*0.9), -- x
			random(mapdef.t, mapdef.b - 2*32)) -- y
		)
		self.tiny.tworld:addEntity(el)
		bworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
	end
	-- _____  _           __     ________ _____  __ 
	--|  __ \| |        /\\ \   / /  ____|  __ \/_ |
	--| |__) | |       /  \\ \_/ /| |__  | |__) || |
	--|  ___/| |      / /\ \\   / |  __| |  _  / | |
	--| |    | |____ / ____ \| |  | |____| | \ \ | |
	--|_|    |______/_/    \_\_|  |______|_|  \_\|_|
	-- EPlayer1:init(xspritelayer, xpos, xbgfxlayer)
	self.player1 = EPlayer1.new(actorslayer, vector(mapdef.l+1*32, mapdef.t+3.2*32), bgfxlayer)
	self.tiny.tworld:addEntity(self.player1)
	bworld:add(self.player1, self.player1.pos.x, self.player1.pos.y, self.player1.collbox.w, self.player1.collbox.h)
	-- _    _ _    _ _____  
	--| |  | | |  | |  __ \ 
	--| |__| | |  | | |  | |
	--|  __  | |  | | |  | |
	--| |  | | |__| | |__| |
	--|_|  |_|\____/|_____/ 
	-- function
	local function map(v, minSrc, maxSrc, minDst, maxDst, clampValue)
		local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
		return not clampValue and newV or clamp(newV, minDst >< maxDst, minDst <> maxDst)
	end
	self.tiny.hud = Sprite.new()
	-- hud lives
	self.tiny.hudlives = {}
	local pixellife
	for i = 1, self.player1.currlives do
		pixellife = Pixel.new(0xffff00, 0.8, 16, 16)
		pixellife:setPosition(8+(i-1)*(16+8), 8)
		self.tiny.hud:addChild(pixellife)
		self.tiny.hudlives[i] = pixellife
	end
	-- hud health
	local hudhealthwidth = map(self.player1.currhealth, 0, self.player1.totalhealth, 0, 100)
	self.tiny.hudhealth = Pixel.new(0x00ff00, 2, hudhealthwidth, 8)
	self.tiny.hudhealth:setPosition(8, 8*3.5)
	self.tiny.hud:addChild(self.tiny.hudhealth)
	-- hud jumps
	self.tiny.hudcurrjumps = TextField.new(myttf2, "JUMPS: "..self.player1.currjumps)
	self.tiny.hudcurrjumps:setTextColor(0xffffff)
	self.tiny.hudcurrjumps:setPosition(8, 8*6.5)
	self.tiny.hud:addChild(self.tiny.hudcurrjumps)
	--  _____          __  __ ______ _____            
	-- / ____|   /\   |  \/  |  ____|  __ \     /\    
	--| |       /  \  | \  / | |__  | |__) |   /  \   
	--| |      / /\ \ | |\/| |  __| |  _  /   / /\ \  
	--| |____ / ____ \| |  | | |____| | \ \  / ____ \ 
	-- \_____/_/    \_\_|  |_|______|_|  \_\/_/    \_\
	-- camera: 'content' is a Sprite which holds all your graphics
	self.camera = GCam.new(mainlayer) -- (content [, anchorX, anchorY]) -- anchor default 0.5, 0.5
	self.camera:setAutoSize(true)
	self.camera:setBounds(myappwidth/2, mapdef.t+camfollowoffsety, mapdef.r-myappwidth/2, mapdef.b) -- left, top, right, bottom
	self.camera:setSoftSize(32, 32*1) -- w, h
	self.camera:setDeadSize(32*1.5, 32*1.5) -- w, h
	self.camera:setFollow(self.player1.sprite)
	self.camera:setFollowOffset(0, camfollowoffsety)
--	self.camera:setDebug(true) -- uncomment for camera debug mode
	--  ____  _____  _____  ______ _____  
	-- / __ \|  __ \|  __ \|  ____|  __ \ 
	--| |  | | |__) | |  | | |__  | |__) |
	--| |  | |  _  /| |  | |  __| |  _  / 
	--| |__| | | \ \| |__| | |____| | \ \ 
	-- \____/|_|  \_\_____/|______|_|  \_\
	if bglayer then mainlayer:addChild(bglayer) end
	if self.extragfx then mainlayer:addChild(self.extragfx) end -- you choose!
	if bgfxlayer then mainlayer:addChild(bgfxlayer) end
	mainlayer:addChild(actorslayer)
	if fgfxlayer then mainlayer:addChild(fgfxlayer) end
	if fglayer then mainlayer:addChild(fglayer) end
	self:addChild(self.camera)
	self:addChild(self.tiny.hud)
	self:addChild(player1inputlayer)
	--  _______     _______ _______ ______ __  __  _____ 
	-- / ____\ \   / / ____|__   __|  ____|  \/  |/ ____|
	--| (___  \ \_/ / (___    | |  | |__  | \  / | (___  
	-- \___ \  \   / \___ \   | |  |  __| | |\/| |\___ \ 
	-- ____) |  | |  ____) |  | |  | |____| |  | |____) |
	--|_____/   |_| |_____/   |_|  |______|_|  |_|_____/ 
	self.tiny.tworld:add(
		-- debug
--		SDebugCollision.new(self.tiny),
--	 	SDebugHurtBoxPlayer.new(self.tiny),
--	 	SDebugHurtBoxNme.new(self.tiny),
--		SDebugHitBoxPlayer.new(self.tiny, { true, true, true, true, false, false } ), -- the 6 abilities (granular debugging)
--		SDebugHitBoxNme.new(self.tiny, { true, true, true, true, true, true } ), -- the 6 abilities (granular debugging)
--		SDebugCollectible.new(self.tiny),
--		SDebugSpriteSorting.new(self.tiny),
		-- systems
		SDrawable.new(self.tiny),
		SAnimation.new(self.tiny),
		SPlayer1.new(self.tiny, self.camera),
		SPlayer1Control.new(self.tiny, player1inputlayer),
		SNmes.new(self.tiny, bworld),
		SAI.new(self.tiny, self.player1),
		SDynamicBodies.new(self.tiny, mapdef),
		SDestructibleObjects.new(self.tiny, bworld),
		SCollectible.new(self.tiny, bworld, self.player1),
		SCollision.new(self.tiny, bworld),
		SHitboxHurtboxCollision.new(self.tiny),
		SShadow.new(self.tiny),
		SSpritesSorting.new(self.tiny)
	)
	-- _      ______ _______ _  _____    _____  ____  _ 
	--| |    |  ____|__   __( )/ ____|  / ____|/ __ \| |
	--| |    | |__     | |  |/| (___   | |  __| |  | | |
	--| |    |  __|    | |     \___ \  | | |_ | |  | | |
	--| |____| |____   | |     ____) | | |__| | |__| |_|
	--|______|______|  |_|    |_____/   \_____|\____/(_)
	self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self) -- the game loop
	self:myKeysPressed() -- keys handler
end

-- _      ____   ____  _____  
--| |    / __ \ / __ \|  __ \ 
--| |   | |  | | |  | | |__) |
--| |   | |  | | |  | |  ___/ 
--| |___| |__| | |__| | |     
--|______\____/ \____/|_|     
local leveltimer = 128 -- 128, 200, you choose
local endleveltimer = leveltimer
local extragfxx = 0
function LevelX:onEnterFrame(e)
	if self.tiny.numberofnmes <= 0 then endleveltimer -= 1 end
	if not ispaused then
		if endleveltimer < 0 then
			g_currlevel += 1
			if g_currlevel > g_totallevels then
				endleveltimer = leveltimer -- reset end level timer
				g_currlevel = 1 -- reset current level
				switchToScene(Win.new()) -- Win
			else
				endleveltimer = leveltimer -- reset end level timer
				switchToScene(LevelX.new()) -- next level
			end
		end
		if self.player1.isactionjump1 or self.player1.isactionjumppunch1 or self.player1.isactionjumpkick1 then
			self.camera:updateXOnly(e.deltaTime)
		else
			self.camera:update(e.deltaTime)
		end
		self.tiny.tworld:update(e.deltaTime) -- tiny world (last)
		if self.extragfx then
			extragfxx = self.extragfx:getX()
			extragfxx -= 196*e.deltaTime
			if extragfxx < -self.extragfx:getWidth() then extragfxx = self.wagonr end
			self.extragfx:setX(extragfxx)
		end
	end
end

-- levels graphics
function LevelX:buildLevel(xbglayer, xfglayer, xmapdef, xactorslayer, xbgfxlayer, xbworld)
	local el
	-- ____   _____ _____   ____  _    _ _   _ _____             _   _ _____  
	--|  _ \ / ____|  __ \ / __ \| |  | | \ | |  __ \      /\   | \ | |  __ \ 
	--| |_) | |  __| |__) | |  | | |  | |  \| | |  | |    /  \  |  \| | |  | |
	--|  _ <| | |_ |  _  /| |  | | |  | | . ` | |  | |   / /\ \ | . ` | |  | |
	--| |_) | |__| | | \ \| |__| | |__| | |\  | |__| |  / ____ \| |\  | |__| |
	--|____/ \_____|_|  \_\\____/ \____/|_| \_|_____/  /_/    \_\_| \_|_____/ 
	-- ______ _____ _____   ____  _    _ _   _ _____  
	--|  ____/ ____|  __ \ / __ \| |  | | \ | |  __ \ 
	--| |__ | |  __| |__) | |  | | |  | |  \| | |  | |
	--|  __|| | |_ |  _  /| |  | | |  | | . ` | |  | |
	--| |   | |__| | | \ \| |__| | |__| | |\  | |__| |
	--|_|    \_____|_|  \_\\____/ \____/|_| \_|_____/ 
	if g_currlevel == 1 then
		-- THE BACKGROUND AND FOREGROUND IF ANY
		el = DrawLevelsTiled.new(xbglayer,
			{
				"gfx/levels/beu_lvl1/untitled_0001.png",
				"gfx/levels/beu_lvl1/untitled_0002.png",
				"gfx/levels/beu_lvl1/untitled_0003.png",
			}, 0*32)
		el = DrawLevelsTiled.new(xfglayer,
			{
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0001.png",
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0002.png",
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0003.png",
			}, 12*32)
		-- map definition (top, left, right, bottom)
		xmapdef.t, xmapdef.l = 9.5*32, 0*32 -- magik XXX
		xmapdef.r, xmapdef.b = el.mapwidth, xmapdef.t + 5.3*32 -- magik XXX
	elseif g_currlevel == 2 then
		-- the background and foreground if any
		el = DrawLevelsTiled.new(xbglayer,
			{
				"gfx/levels/beu_lvl2/untitled_0001.png",
				"gfx/levels/beu_lvl2/untitled_0002.png",
				"gfx/levels/beu_lvl2/untitled_0003.png",
			}, 0*32)
		el = DrawLevelsTiled.new(xfglayer,
			{
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0001.png",
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0002.png",
				"gfx/levels/beu_lvl1/beu_fg_lvl1_0003.png",
			}, 12*32)
		-- map definition (top, left, right, bottom)
		xmapdef.t, xmapdef.l = 8.55*32, 0*32 -- magik XXX
		xmapdef.r, xmapdef.b = el.mapwidth, xmapdef.t + 6*32 -- magik XXX
	elseif g_currlevel == 3 then
		-- the background and foreground if any
		el = DrawLevelsTiled.new(xbglayer,
			{
				"gfx/levels/beu_lvl3/untitled_0001.png",
				"gfx/levels/beu_lvl3/untitled_0002.png",
				"gfx/levels/beu_lvl3/untitled_0003.png",
			}, 0*32)
		el = DrawLevelsTiled.new(xfglayer,
			{
				"gfx/levels/beu_lvl3/untitledfg_0001.png",
				"gfx/levels/beu_lvl3/untitledfg_0002.png",
				"gfx/levels/beu_lvl3/untitledfg_0003.png",
			}, 0*32)
		xfglayer:setAlpha(0.87)
		-- map definition (top, left, right, bottom)
		xmapdef.t, xmapdef.l = 8.9*32, 0*32 -- magik XXX
		xmapdef.r, xmapdef.b = el.mapwidth, xmapdef.t + 6*32 -- magik XXX
	end
	-- _______ _    _ ______   ______ _   _ ______ __  __ _____ ______  _____ 
	--|__   __| |  | |  ____| |  ____| \ | |  ____|  \/  |_   _|  ____|/ ____|
	--   | |  | |__| | |__    | |__  |  \| | |__  | \  / | | | | |__  | (___  
	--   | |  |  __  |  __|   |  __| | . ` |  __| | |\/| | | | |  __|  \___ \ 
	--   | |  | |  | | |____  | |____| |\  | |____| |  | |_| |_| |____ ____) |
	--   |_|  |_|  |_|______| |______|_| \_|______|_|  |_|_____|______|_____/ 
	-- ENmeX:init(xspritelayer, x, y, dx, dy, xbgfxlayer)
	if g_currlevel == 1 then
		for i = 1, 6 do
			el = ENme1.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.3, (xmapdef.r-xmapdef.l)*0.7), random(xmapdef.t, xmapdef.b-1*32)),
				random(12, 24)*16, random(myappheight*0.2), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
		for i = 1, 6 do
			el = ENme2.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.75, (xmapdef.r-xmapdef.l)*1), random(xmapdef.t, xmapdef.b-2*32)),
				random(12, 24)*16, random(myappheight*0.1), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
	elseif g_currlevel == 2 then
		for i = 1, 7 do
			el = ENme1.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.25, (xmapdef.r-xmapdef.l)*0.75), random(xmapdef.t, xmapdef.b-1*32)),
				random(12, 24)*16, random(myappheight*0.2), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
		for i = 1, 7 do
			el = ENme3.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.75, (xmapdef.r-xmapdef.l)*1), random(xmapdef.t, xmapdef.b-1*32)),
				random(12, 24)*16, random(myappheight*0.1), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
	elseif g_currlevel == 3 then
		for i = 1, 8 do
			el = ENme1.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.2, (xmapdef.r-xmapdef.l)*0.75), random(xmapdef.t, xmapdef.b-1*32)),
				random(12, 24)*16, random(myappheight*0.2), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
		for i = 1, 8 do
			el = ENme3.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.5, (xmapdef.r-xmapdef.l)*0.9), random(xmapdef.t, xmapdef.b-1*32)),
				random(12, 24)*16, random(myappheight*0.1), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
		for i = 1, 3 do
			el = ENme4.new(xactorslayer,
				vector(random((xmapdef.r-xmapdef.l)*0.8, (xmapdef.r-xmapdef.l)*1), random(xmapdef.t, xmapdef.b-2*32)),
				random(12, 24)*16, random(myappheight*0.1), xbgfxlayer)
			self.tiny.tworld:addEntity(el)
			self.tiny.numberofnmes += 1
			xbworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
		end
	end
	-- ________   _________ _____               _____ ________   __
	--|  ____\ \ / /__   __|  __ \     /\      / ____|  ____\ \ / /
	--| |__   \ V /   | |  | |__) |   /  \    | |  __| |__   \ V / 
	--|  __|   > <    | |  |  _  /   / /\ \   | | |_ |  __|   > <  
	--| |____ / . \   | |  | | \ \  / ____ \  | |__| | |     / . \ 
	--|______/_/ \_\  |_|  |_|  \_\/_/    \_\  \_____|_|    /_/ \_\
	if g_currlevel == 1 then
		-- nothing here
	elseif g_currlevel == 2 then
		-- nothing here
	elseif g_currlevel == 3 then
		-- a moving wagon
		self.extragfx = Bitmap.new(Texture.new("gfx/levels/beu_lvl3/subway_car.001_0013.png"))
		self.extragfx:setScale(0.65)
		self.wagonr = xmapdef.r
		self.extragfx:setPosition(random(xmapdef.r*0.5, xmapdef.r), 1*myappheight/10)
	end
	-- cleaning?
	el = nil
	collectgarbage()
end

-- keys handler
function LevelX:myKeysPressed()
	self:addEventListener(Event.KEY_DOWN, function(e) -- KEY_UP
		-- Menu
		if e.keyCode == KeyCode.ESC or e.keyCode == KeyCode.BACK then switchToScene(Menu.new()) end
		-- pause
		if e.keyCode == KeyCode.P then ispaused = not ispaused end
		-- modifiers
		local modifier = application:getKeyboardModifiers()
		local alt = (modifier & KeyCode.MODIFIER_ALT) > 0
		-- nothing here!
		if not alt and e.keyCode == KeyCode.ENTER then
		-- switch fullscreen
		elseif alt and e.keyCode == KeyCode.ENTER then
			ismyappfullscreen = not ismyappfullscreen
			application:setFullScreen(ismyappfullscreen)
		end
	end)
end

levelX.lua Code comments

Let's break it down!

The --!strict thing is some Luau stuff I was messing around with, you can find more information here: https://devforum.roblox.com/t/luau-type-checking-beta/435382#strict-mode-5.

I declare 2 variables which are local to the Class because I didn't want to use self on them. The random variable caches the math.random function for speed. I am also experimenting here with Luau type annotations https://devforum.roblox.com/t/luau-type-checking-beta/435382#new-syntax-6.

local random = math.random
local ispaused : boolean = false

LevelX:init()

I commented out the "move cursor" code because it was more annoying than anything else! You can use it if you want.

plugins

Here we initialize our plugins. tiny-ecs is declared as a Class variable (self.tiny) because we use it outside the init function. Bump can be local as we only use it in the init function.

It is worth noting that tiny.tworld is attached to self.tiny variable. This makes it much easier to access through the rest of the code in our project.

layers

This one is easy :-)

We create several layers which will be laid out on top of each other. The background layer will have all the graphics for the background, etc...

There is a player1inputlayer which will capture the user input to control the player, it won't hold any graphics.

levels

Time to build our levels. The sprite list is a table of all the actors we can interact with (the player, the nmes, collectibles, ...). We put them in a list so we can use that list in the systems we will create (sAI, sAnimation, sCollectible, sCollision, ...).

tiny.numberofnmes and tiny.numberofdestructibleobjects are variables we can tune to add a certain amount of enemies and destructible objects in each level.

The mapdef is a table which has the map definition (dimensions), so we can pass it to functions that will require the size of the map for calculations.

We use a camera in our game and camfollowoffsety will offset the camera following the player. It is easier to change it being a variable.

The buildLevel function is where we build the current level the player is playing. We pass it some variables so it can do its thing. Some code comments on that function below.

Then we set the variables for each level: the camera offset and the number of destructible objects. The destructible objects spawn collectible when they are destroyed (extended life, jumps).

We randomly place the destructible objects throughout the level using the map definition. We place them between 25% and 90% of the map length and a little bit above the bottom of the map.

Each destructible objects are entities and we add them to both the tiny-ECS world and the Bump world. We will create entities in the coming parts of the tutorial.

player1

The player is an ECS entity we will create in the next chapters. The arguments to the init function are: the layer the player sprite will be added to, the position as a vector, and the layer for any fancy graphics effects we may add to the player.

After the entity is created, we add it to both the tiny-ECS world and the Bump world.

hud

I added a simple head up display to the game, so we can see the player current health, number of lives and number of attacking jumps available.

The same way we attached tiny.tworld to the self.tiny variable, we attach some more variables to it. This makes it easier to access those variables.

the camera

We use a slightly modified version of MultiPain's (aka rrraptor on Gideros forum) GCam Class for our camera.

Please grab it here Media:gcam_beu.lua (tip: right click and save link as) and put it in the "classes" folder.

We pass the mainlayer as the content, then using the map definition we set the camera bounds. We also set the soft and the dead size parameters and we tell it to follow the player.

if you are curious I added an extra updateXOnly function ;-)

order

This is the order of the layers, from bottom to top. We first add the background layer and the other layers on top of it.

systems

Once all entities are done, we add the ECS systems. We will see those ECS systems in the coming parts of the tutorial.

let's go

Finally we are ready to run the game loop!

We also listen to some key events to pause the game, go fullscreen, ...

LevelX:buildLevel

Let's have a look at how we construct our levels.

background and foreground

We draw the background and foreground for each level using the DrawLevelsTiled Class. Each level will have its own graphics. A typical level is 3*1024 pixels wide. Using a power of two size for the graphics, we anticipate any kind of optimisations we may need, plus adding the possibility to port the game to mobile!

The DrawLevelsTiled Class already tries to bring in some optimisations! You can create a file called "drawlevelstiled.lua" and put it in the gfx\levels\ folder. The code:

DrawLevelsTiled = Core.class(Sprite)

function DrawLevelsTiled:init(xlayer, xtexpaths, xposy)
	-- tilemaps textures
	local textures = {}
	for i = 1, #xtexpaths do
--		textures[i] = Texture.new(xtexpaths[i])
--		textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.YA8}) -- best win32 perfs but b&w!
		textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA4444}) -- better win32 perfs but !
--		textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA5551}) -- better win32 perfs but !
	end
	-- map size
	local tilesizetarget = 64
	local tilesetcols, tilesetrows = textures[1]:getWidth()/tilesizetarget, textures[1]:getHeight()/tilesizetarget
	-- create the tilemaps
	local function createTilemap(xtex)
		local tm = TileMap.new(
			tilesetcols, tilesetrows, -- map size in tiles
			xtex, -- tileset texture
			tilesizetarget, tilesizetarget -- tile size in pixel
		)
		-- build the map
		for i=1,tilesetcols do
			for j=1,tilesetrows do
				tm:setTile(i, j, i, j)
			end
		end
		return tm
	end
	-- the maps
	for i = 1, #textures do
		local map = createTilemap(textures[i])
		map:setPosition(map:getWidth()*(i-1), xposy)
--		self:addChild(map)
		xlayer:addChild(map)
	end
	-- params
--	self.mapwidth = self:getWidth()
--	self.mapheight = self:getHeight()
	self.mapwidth = xlayer:getWidth()
	self.mapheight = xlayer:getHeight()
	-- clean
	textures = {}
end

As the name tries to suggest (DrawLevelsTiled), we take the paths to the graphics and create tiles as if we were drawing using a tilemap. The init function parameters are: the layer we add the sprites to, the paths to the textures as a table and an y offset for flexibility.

The first step is to create the textures and put them in a table called textures. When we iterate through the xtexpaths table, I experimented with some texture formats to see if I could gain some speed.

Once the textures are created and stored in a table, we can create a TileMap of 64*64px tiles.

Finally we return the size of the map and we clean the textures table just in case.

Back to LevelX Class, now that our background and foreground layers are drawn, we set the map definition. The map definition sets the boundaries of the map where the actors can freely roam.

the enemies

Each level will have different kind of enemies. The first level should be easy with few enemies you can easily kill. The other levels will be increasingly difficult with more enemies a little bit harder to defeat.

So here, depending on the level we create a bunch of enemies. Enemies are ECS entities we will create in the following chapters. The game has four types of enemies of varying strength. We add them to both the tiny-ECS world and the Bump world.

extra gfx

Depending on the level, we may have extra sprites we want to add to the level.

We finish the function with some house cleaning.

LevelX:onEnterFrame THE GAME LOOP

Before the game loop, I declare three local variables: leveltimer is the time it takes to transition to the next level, endleveltimer is the timer itself, extragfxx is to move the extra sprite we may have on the x axis.

When we defeat all enemies, the timer decreases. When it reaches 0, the current level number is increased and we load it.

When the player is jumping we update the camera only on the x axis, otherwise we update the camera on both x and y axis.

We update the tiny-ecs world, so it can run all the systems (animations, movements, AI, ...). Some systems will be called only once, others every frame per second.

LevelX:myKeysPressed

The last function of the LevelX Class is myKeysPressed.

Here we simply listen to some KEY_DOWN Events, that is keys pressed on the keyboard. The key ESC will go back to the menu, the letter P will pause the game and when you press ALT+ENTER you switch the game to fullscreen.

Next?

That was quite a lot of work but we coded the heart of our game and we are already nearly done!!

All is left to do is add the actors. Actors will be ECS entites, those entities will have components and systems will control them.

In the next part we deal with our player1 entity.


Prev.: Tuto tiny-ecs beatemup Part 3 transitions menu options
Next: Tuto tiny-ecs beatemup Part 5 ePlayer1


Tutorial - tiny-ecs beatemup