Difference between revisions of "Tuto tiny-ecs 2d platformer Part 4 LevelX"

From GiderosMobile
(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