Difference between revisions of "Tuto tiny-ecs 2d platformer Part 4 LevelX"
(wip) |
(No difference)
|
Revision as of 04:26, 6 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 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 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: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 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
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 will explain more about it in the next chapter of the tutorial
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.
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 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 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.
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
Let's have a look at how we construct our levels.
The code:
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