Difference between revisions of "Tuto tiny-ecs 2d platformer Part 4 LevelX"
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 | + | When the scene loads, it constructs the level 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). | 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). | ||
Line 241: | Line 241: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === LevelX:init | + | == LevelX Code comments == |
+ | === LevelX:init === | ||
==== plugins ==== | ==== 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. | 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 | + | It is worth noting that ''tiny.tworld'' is attached to ''self.tiny'' variable. This makes it easier to access through the rest of the code in our project. |
==== layers ==== | ==== layers ==== | ||
Line 257: | Line 258: | ||
We use '''Tiled''' to build our levels (https://www.mapeditor.org/) and we call the ''TiledLevels Class'' to manage the parsing of the Tiled elements in our game. | We use '''Tiled''' to build our levels (https://www.mapeditor.org/) and we call the ''TiledLevels Class'' to manage the parsing of the Tiled elements in our game. | ||
− | '''We dedicate an entire folder for Tiled in our project and we | + | '''We dedicate an entire folder for Tiled in our project and we talk more about it down below''' |
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. | 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. | ||
Line 272: | Line 273: | ||
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. | ||
− | We use a slightly modified version of '''MultiPain''''s (aka rrraptor on Gideros forum) '''GCam''' Class for our camera. | + | We use a slightly modified version of '''MultiPain''''s (aka rrraptor on Gideros forum) '''GCam''' Class for our camera. We pass it the ''mainlayer'' as the content and the player1 to follow. |
− | |||
− | |||
− | |||
− | We pass the ''mainlayer'' as the content and the player1 to follow. | ||
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. | 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. | ||
Line 305: | Line 302: | ||
== TiledLevels Class == | == TiledLevels Class == | ||
+ | To better organise the game, I put all files related to '''Tiled''' in a '''tiled''' folder. | ||
+ | |||
Let's have a look at how we construct our levels. | Let's have a look at how we construct our levels. | ||
− | + | In Tiled we add many layers, in our case '''Object Layers'''. Each layers have a type and a name (eg. ''physics_ptpfs'', ''physics_coins'', ''bg_deco_images'', ...). In each layers we draw the level using shapes (rectangles, ellipses, ...). Shapes are also used to position the actors in the level, like the player, the enemies, moving platforms, ... | |
+ | |||
+ | Once the level is done in Tiled, we export it as a '''lua''' file Gideros will use (read the layers name, the shapes, the color, the position, ...). | ||
+ | |||
+ | === TiledLevels Class Code comments === | ||
+ | The very first thing we do is include the Tiled lua file. I also set up some variables to check if we are testing a prototype level and if we are in development mode. | ||
+ | |||
+ | ==== Tileset ==== | ||
+ | This is the classic: a tileset with a tilemap image ('''Tile Layer''' in Tiled). We don't use tilsets in this game but feel free to do so. The code extracts the tilemap info: number of columns, rows, tile id (gid) and texture. | ||
− | + | ==== Tileset images ==== | |
+ | Instead of tilesets we use single images to make our levels. Here we read the gid and the image path and store them in tables. The tables are then turned into '''[[TexturePack]]''' so the game runs faster. | ||
− | + | ==== Build Level ==== | |
+ | To build a level, we iterate all the tilemap layers according to their '''type''' and their '''name'''. We either build shapes the '''physics world''' can use or we add images to decorate the level using the ''parseImage'' function. | ||
− | '''note''': we | + | '''note''': we don't use Tiled ''Tile Layer'' in this game but you can, Build Level takes care of it for you in ''layer.type == "tilelayer"'' |
− | To | + | To build the shapes in the game we use the Gideros '''[[Shape]]''' class. I have written several classes for each one of them: ''Tiled_Shape_Ellipse'', ''Tiled_Shape_Point'', ''Tiled_Shape_Polygon'', ... . When we read the Tiled file we look for the layer name, if the layer contains shapes, we call the appropriate shape class and voilà :-). The ''buildShapes'' function is at the very bottom of the class. |
− | You can read more about Tiled and Gideros here: [[Tiled_Bump]]. | + | That's basically how we build our levels using Tiled. You can read more about Tiled and Gideros here: [[Tiled_Bump]]. |
== Next? == | == Next? == |
Latest revision as of 03:21, 7 September 2025
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 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 code:
LevelX = Core.class(Sprite)
local ispaused = false
function LevelX:init()
-- 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-10, sh-10) -- 0, 0
end
-- _____ _ _ _ _____ _____ _ _ _____
--| __ \| | | | | |/ ____|_ _| \ | |/ ____|
--| |__) | | | | | | | __ | | | \| | (___
--| ___/| | | | | | | |_ | | | | . ` |\___ \
--| | | |___| |__| | |__| |_| |_| |\ |____) |
--|_| |______\____/ \_____|_____|_| \_|_____/
-- tiny-ecs
if not self.tiny then self.tiny = require "classes/tiny-ecs" end
self.tiny.tworld = self.tiny.world()
-- cbump (bworld)
local bump = require "cbump"
local bworld = bump.newWorld() -- 16, grid cell size, default = 64
-- _ __ ________ _____ _____
--| | /\\ \ / / ____| __ \ / ____|
--| | / \\ \_/ /| |__ | |__) | (___
--| | / /\ \\ / | __| | _ / \___ \
--| |____ / ____ \| | | |____| | \ \ ____) |
--|______/_/ \_\_| |______|_| \_\_____/
local layers = {}
layers["main"] = Sprite.new() -- one Sprite to hold them all
layers["bg"] = Sprite.new() -- bg layer
layers["bgfx"] = Sprite.new() -- bg fx layer
layers["actors"] = Sprite.new() -- actors layer
layers["fgfx"] = Sprite.new() -- fg fx layer
layers["fg"] = Sprite.new() -- fg layer
layers["player1input"] = Sprite.new() -- player1 input layer
-- _ ________ ________ _ _____
--| | | ____\ \ / / ____| | / ____|
--| | | |__ \ \ / /| |__ | | | (___
--| | | __| \ \/ / | __| | | \___ \
--| |____| |____ \ / | |____| |____ ____) |
--|______|______| \/ |______|______|_____/
self.tiledlevels = {}
self.tiledlevels[1] = "tiled/lvl001/_level1_proto" -- lua file without extension
self.tiledlevels[2] = "tiled/lvl001/level1" -- lua file without extension
-- self.tiledlevels[3] = "tiled/lvl002/level1_proto" -- lua file without extension
local mapdef = {} -- game area (rect: top, left, right, bottom)
local zoom = 1 -- 1.3, 1.5, 1.7
local camfollowoffsety = 28 -- 32, 36, 48, camera follow player1 y offset, magik XXX
self.tiny.player1inventory = {} -- player1 inventory
self.tiny.numberofcoins = 0
local currlevel = TiledLevels.new(
self.tiledlevels[g_currlevel], self.tiny, bworld, layers
)
-- currlevel map definition (top, left, right, bottom) for the camera
mapdef.t, mapdef.l =
currlevel.mapdef.t, currlevel.mapdef.l
mapdef.r, mapdef.b =
currlevel.mapdef.l+currlevel.mapdef.r, currlevel.mapdef.t+currlevel.mapdef.b
-- _____ _ __ ________ _____ __
--| __ \| | /\\ \ / / ____| __ \/_ |
--| |__) | | / \\ \_/ /| |__ | |__) || |
--| ___/| | / /\ \\ / | __| | _ / | |
--| | | |____ / ____ \| | | |____| | \ \ | |
--|_| |______/_/ \_\_| |______|_| \_\|_|
self.player1 = currlevel.player1
-- _ _ _ _ _____
--| | | | | | | __ \
--| |__| | | | | | | |
--| __ | | | | | | |
--| | | | |__| | |__| |
--|_| |_|\____/|_____/
-- function
local function clamp(v, mn, mx) return (v><mx)<>mn end
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, true)
self.tiny.hudhealth = Pixel.new(0x00ff00, 2, hudhealthwidth, 8)
self.tiny.hudhealth:setPosition(8*1, 8*3.5)
self.tiny.hud:addChild(self.tiny.hudhealth)
-- hud coins
self.tiny.hudcoins = TextField.new(cf2, "COINS: "..self.tiny.numberofcoins)
self.tiny.hudcoins:setTextColor(0xff5500)
self.tiny.hudcoins:setPosition(8*20, 8*2.3)
self.tiny.hud:addChild(self.tiny.hudcoins)
-- _____ __ __ ______ _____
-- / ____| /\ | \/ | ____| __ \ /\
--| | / \ | \ / | |__ | |__) | / \
--| | / /\ \ | |\/| | __| | _ / / /\ \
--| |____ / ____ \| | | | |____| | \ \ / ____ \
-- \_____/_/ \_\_| |_|______|_| \_\/_/ \_\
-- camera: 'content' is a Sprite which holds all your graphics
self.camera = GCam.new(layers["main"], nil, nil, self.player1) -- (content [, anchorX, anchorY]) -- anchor default 0.5, 0.5
self.camera:setAutoSize(true)
self.camera:setZoom(zoom)
self.camera:setBounds(
mapdef.l+myappwidth/2/zoom, -- left
mapdef.t+myappheight/2/zoom, -- top
mapdef.r-myappwidth/2/zoom, -- right
mapdef.b-myappheight/2/zoom -- bottom
)
self.camera:setSoftSize(1.5*32, 2.2*32) -- 1.5*32, 3*32, w, h
self.camera:setDeadSize(3*32, 4.4*32) -- 3*32, 5*32, w, h
self.camera:setFollow(self.player1.sprite)
self.camera:setFollowOffset(0, camfollowoffsety)
self.camera:setPredictMode(true)
self.camera:setPrediction(0.9) -- 0.8, 0.75, 0.5, number betwwen 0 and 1
self.camera:lockPredictionY() -- no prediction on Y axis
self.camera:setPredictionSmoothing(4) -- 8, 4, smooth prediction
-- self.camera:setDebug(true) -- uncomment for camera debug mode
-- ____ _____ _____ ______ _____
-- / __ \| __ \| __ \| ____| __ \
--| | | | |__) | | | | |__ | |__) |
--| | | | _ /| | | | __| | _ /
--| |__| | | \ \| |__| | |____| | \ \
-- \____/|_| \_\_____/|______|_| \_\
layers["main"]:addChild(layers["bg"])
layers["main"]:addChild(layers["bgfx"])
layers["main"]:addChild(layers["actors"])
layers["main"]:addChild(layers["fgfx"])
layers["main"]:addChild(layers["fg"])
self:addChild(self.camera)
self:addChild(self.tiny.hud)
self:addChild(layers["player1input"])
-- _______ _______ _______ ______ __ __ _____
-- / ____\ \ / / ____|__ __| ____| \/ |/ ____|
--| (___ \ \_/ / (___ | | | |__ | \ / | (___
-- \___ \ \ / \___ \ | | | __| | |\/| |\___ \
-- ____) | | | ____) | | | | |____| | | |____) |
--|_____/ |_| |_____/ |_| |______|_| |_|_____/
self.tiny.tworld:add(
-- debug
-- SDebugPosition.new(self.tiny),
-- SDebugCollision.new(self.tiny),
-- SDebugShield.new(self.tiny),
-- systems
SDrawable.new(self.tiny),
SAnimation.new(self.tiny),
SPlayer1.new(self.tiny, bworld, self.camera),
SPlayer1Control.new(self.tiny, self.camera, mapdef, player1inputlayer),
SNmes.new(self.tiny, bworld, self.player1),
SAI.new(self.tiny, self.player1),
SSensor.new(self.tiny, bworld, self.player1),
SDoor.new(self.tiny, bworld, self.player1),
SMvpf.new(self.tiny, bworld),
SCollectibles.new(self.tiny, bworld, self.player1),
SProjectiles.new(self.tiny, bworld),
SOscillation.new(self.tiny, bworld, self.player1),
SCollision.new(self.tiny, bworld, self.player1)
)
-- _ ______ _______ _ _____ _____ ____ _
--| | | ____|__ __( )/ ____| / ____|/ __ \| |
--| | | |__ | | |/| (___ | | __| | | | |
--| | | __| | | \___ \ | | |_ | | | | |
--| |____| |____ | | ____) | | |__| | |__| |_|
--|______|______| |_| |_____/ \_____|\____/(_)
self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self) -- the game loop
self:myKeysPressed() -- keys handler
end
-- game loop
local timer = 40*8 -- magik XXX
function LevelX:onEnterFrame(e)
if not ispaused then
local dt = e.deltaTime
if self.player1.restart then
if g_currlevel > #self.tiledlevels then
timer -= 1
if self.camera:getZoom() < 2 then
self.camera:setZoom(self.camera:getZoom()+0.4*dt)
end
if timer <= 0 then
self:gotoScene(Win.new())
timer = 40*8
end
else
self:gotoScene(LevelX.new())
end
self.camera:setPredictionSmoothing(1) -- 4, smooth prediction
self.camera:update(dt) -- e.deltaTime, 1/60
else
self.camera:update(dt) -- e.deltaTime, 1/60
self.tiny.tworld:update(dt) -- tiny world (last)
end
end
end
-- keys handler
function LevelX:myKeysPressed()
self:addEventListener(Event.KEY_DOWN, function(e) -- KEY_UP
if e.keyCode == KeyCode.ESC or e.keyCode == KeyCode.BACK then -- MENU
self:gotoScene(Menu.new())
elseif e.keyCode == KeyCode.P then -- PAUSE
ispaused = not ispaused
elseif e.keyCode == KeyCode.R then -- RESTART
self.player1.restart = true
end
-- modifier
local modifier = application:getKeyboardModifiers()
local alt = (modifier & KeyCode.MODIFIER_ALT) > 0
if (not alt and e.keyCode == KeyCode.ENTER) then -- nothing here!
elseif alt and e.keyCode == KeyCode.ENTER then -- SWITCH FULLSCREEN
ismyappfullscreen = not ismyappfullscreen
application:setFullScreen(ismyappfullscreen)
end
end)
end
-- scenes navigation
function LevelX:gotoScene(xscene)
switchToScene(xscene) -- next scene
end
LevelX Code comments
LevelX:init
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 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.
Tiled levels
We use Tiled to build our levels (https://www.mapeditor.org/) and we call the TiledLevels Class to manage the parsing of the Tiled elements in our game.
We dedicate an entire folder for Tiled in our project and we talk more about it down below
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.
player1
The player is an ECS entity we create in the TiledLevels Class, more on that in the next chapters. We need it here because the HUD and the camera depend on it.
hud
I added a simple head up display to the game, so we can see the player current health, number of lives, ...
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 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 slightly modified version of MultiPain's (aka rrraptor on Gideros forum) GCam Class for our camera. We pass it the mainlayer as the content and the player1 to follow.
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.
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:onEnterFrame THE GAME LOOP
Before the game loop, I declare a local variable timer which is the time before transitioning to the next level.
If we beat the level then we go to the next level or the Win scene.
Otherwise, if the game is not paused we update the camera and the tiny-ecs world. tiny-ecs world runs all the systems (animations, movements, AI, ...). Some systems will be called only once, others every frame per second. More on Systems later on in this tutorial.
LevelX: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.
LevelX:gotoScene
This is where we transition to the next scene calling the global function switchToScene.
TiledLevels Class
To better organise the game, I put all files related to Tiled in a tiled folder.
Let's have a look at how we construct our levels.
In Tiled we add many layers, in our case Object Layers. Each layers have a type and a name (eg. physics_ptpfs, physics_coins, bg_deco_images, ...). In each layers we draw the level using shapes (rectangles, ellipses, ...). Shapes are also used to position the actors in the level, like the player, the enemies, moving platforms, ...
Once the level is done in Tiled, we export it as a lua file Gideros will use (read the layers name, the shapes, the color, the position, ...).
TiledLevels Class Code comments
The very first thing we do is include the Tiled lua file. I also set up some variables to check if we are testing a prototype level and if we are in development mode.
Tileset
This is the classic: a tileset with a tilemap image (Tile Layer in Tiled). We don't use tilsets in this game but feel free to do so. The code extracts the tilemap info: number of columns, rows, tile id (gid) and texture.
Tileset images
Instead of tilesets we use single images to make our levels. Here we read the gid and the image path and store them in tables. The tables are then turned into TexturePack so the game runs faster.
Build Level
To build a level, we iterate all the tilemap layers according to their type and their name. We either build shapes the physics world can use or we add images to decorate the level using the parseImage function.
note: we don't use Tiled Tile Layer in this game but you can, Build Level takes care of it for you in layer.type == "tilelayer"
To build the shapes in the game we use the Gideros Shape class. I have written several classes for each one of them: Tiled_Shape_Ellipse, Tiled_Shape_Point, Tiled_Shape_Polygon, ... . When we read the Tiled file we look for the layer name, if the layer contains shapes, we call the appropriate shape class and voilà :-). The buildShapes function is at the very bottom of the class.
That's basically how we build our levels using Tiled. You can read more about Tiled and Gideros here: Tiled_Bump.
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 2d platformer Part 3 transitions menu options
Next: Tuto tiny-ecs 2d platformer Part 5 ePlayer1
Tutorial - tiny-ecs 2d platformer