Tuto tiny-ecs 2d platformer Part 11 Systems 3

From GiderosMobile
Revision as of 19:28, 8 November 2025 by MoKaLux (talk | contribs) (wip)

The Systems 3

A couple more systems to add and we are done. We also add the Debug Systems to help us visualize the collision boxes.

sCollectibles.lua

Let's start with the collectibles, you know those things you can pick up 😊.

Please create a file "sCollectibles.lua" in the "_S" folder and the code:

SCollectibles = Core.class()

function SCollectibles:init(xtiny, xbump, xplayer1) -- tiny function
	self.tiny = xtiny -- make a class ref
	self.tiny.processingSystem(self) -- called once on init and every update
	self.bworld = xbump
	self.player1 = xplayer1
	-- sfx
	self.snd = { sound=Sound.new("audio/sfx/sfx_coin_double1.wav"), time=0, delay=0.2, }
end

function SCollectibles:filter(ent) -- tiny function
	return ent.iscollectible
end

function SCollectibles:onAdd(ent) -- tiny function
end

function SCollectibles:onRemove(ent) -- tiny function
	self.bworld:remove(ent) -- remove collision box from cbump world here!
end

function SCollectibles:process(ent, dt) -- tiny function
	if ent.isdirty then -- hit
		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
		local snd = self.snd -- sfx
		local curr = os.timer()
		local prev = snd.time
		if curr - prev > snd.delay then
			local channel = snd.sound:play()
			if channel then channel:setVolume(g_sfxvolume*0.01) end
			snd.time = curr
		end
		if ent.eid == "coins" then -- coins
			self.tiny.numberofcoins += 1
			self.tiny.hudcoins:setText("COINS: "..self.tiny.numberofcoins)
		elseif ent.eid == "lives" then -- hearts
			self.player1.currhealth += 1
			-- hud
			local hudhealthwidth = map(self.player1.currhealth, 0, self.player1.totalhealth, 0, 100)
			self.tiny.hudhealth:setWidth(hudhealthwidth)
			if self.player1.currhealth < self.player1.totalhealth/3 then self.tiny.hudhealth:setColor(0xff0000)
			elseif self.player1.currhealth < self.player1.totalhealth/2 then self.tiny.hudhealth:setColor(0xff5500)
			else self.tiny.hudhealth:setColor(0x00ff00)
			end
		elseif ent.eid == "dash" then -- ???
			self.player1.body.candash = true
		end
		self.tiny.tworld:removeEntity(ent) -- sprite is removed in SDrawable
	end
end

What it does:

  • depending on the item collected (coin, live, ...) it increases a value
  • the UI is updated as well to reflect the new values
  • the collectible is removed from the scene in the onRemove function

sDoor.lua

Doors will work in tandem with sensors and keys collectible. A door can open in all directions: up, down, left, right and diagonals.

Please create a file "sDoor.lua" in the "_S" folder and the code:

SDoor = Core.class()

function SDoor:init(xtiny, xbworld, xplayer1)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.tiny = xtiny
	self.bworld = xbworld -- cbump world
	self.player1 = xplayer1
end

function SDoor:filter(ent) -- tiny function
	return ent.isdoor
end

function SDoor:onAdd(ent) -- tiny function
--	print("SDoor:onAdd")
end

function SDoor:onRemove(ent) -- tiny function
--	print("SDoor:onRemove")
end

local p1rangetoofarx = myappwidth*1 -- disable systems to save some CPU, magik XXX
local p1rangetoofary = myappheight*1 -- disable systems to save some CPU, magik XXX
function SDoor:process(ent, dt) -- tiny function
	local function fun()
		-- OUTSIDE VISIBLE RANGE
		if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or
			ent.pos.x < self.player1.pos.x - p1rangetoofarx) or
			(ent.pos.y > self.player1.pos.y + p1rangetoofary or
			ent.pos.y < self.player1.pos.y - p1rangetoofary) then
			ent.doanimate = false
			return
		end
		ent.isactive = false
		ent.isup = false ent.isdown = false
		ent.isleft = false ent.isright = false
		if ent.eid == self.player1.insensor then -- player1 inside door sensor, open
			if ent.eid:find("Z") or -- no key needed for eid containing 'Z'
				(self.tiny.player1inventory[ent.eid]) then -- player1 has key in inventory
				-- left/right opening
				if ent.dir:match("L") then
					if ent.pos.x > ent.distance.startpos.x - ent.distance.dx then -- move left
						ent.isleft = true ent.isright = false
						ent.isactive = true
					end
				elseif ent.dir:match("R") then
					if ent.pos.x < ent.distance.startpos.x + ent.distance.dx then -- move right
						ent.isright = true ent.isleft = false
						ent.isactive = true
					end
				end
				-- up/down opening
				if ent.dir:match("U") then
					if ent.pos.y > ent.distance.startpos.y - ent.distance.dy then -- move up
						ent.isup = true ent.isdown = false
						ent.isactive = true
					end
				elseif ent.dir:match("D") then
					if ent.pos.y < ent.distance.startpos.y + ent.distance.dy then -- move down
						ent.isdown = true ent.isup = false
						ent.isactive = true
					end
				end
			end
		else -- player1 outside door sensor, close
			-- left/right closing
			if ent.dir:match("L") then
				if ent.pos.x < ent.distance.startpos.x then -- move right
					ent.isright = true ent.isleft = false
					ent.isactive = true
				end
			elseif ent.dir:match("R") then
				if ent.pos.x > ent.distance.startpos.x then -- move left
					ent.isleft = true ent.isright = false
					ent.isactive = true
				end
			end
			-- up/down closing
			if ent.dir:match("U") then
				if ent.pos.y < ent.distance.startpos.y then -- move down
					ent.isdown = true ent.isup = false
					ent.isactive = true
				end
			elseif ent.dir:match("D") then
				if ent.pos.y > ent.distance.startpos.y - ent.distance.dy then -- move up
					ent.isup = true ent.isdown = false
					ent.isactive = true
				end
			end
		end
		-- if player1 acquires key, illuminate (cutscenes, ...)
		if (ent.eid:find("Z") and not ent.highlight) or -- no key needed for eid containing 'Z'
			(self.tiny.player1inventory[ent.eid] and not ent.highlight) then -- player1 has key
			ent.sprite:setAlpha(1.7) -- 1.6, 2
			ent.highlight = true -- only highlight once
		end
		-- action
		if ent.isactive then
			local function collisionfilter(item, other) -- "touch", "cross", "slide", "bounce"
				if other.isnme or other.isplayer1 then return "slide" end -- ok
				return nil -- "cross"
			end
			--  _____ ____  _    _ __  __ _____  
			-- / ____|  _ \| |  | |  \/  |  __ \ 
			--| |    | |_) | |  | | \  / | |__) |
			--| |    |  _ <| |  | | |\/| |  ___/ 
			--| |____| |_) | |__| | |  | | |     
			-- \_____|____/ \____/|_|  |_|_|     
			local goalx = ent.pos.x + ent.body.vx * dt
			local goaly = ent.pos.y + ent.body.vy * dt
			local nextx, nexty = self.bworld:move(ent, goalx, goaly, collisionfilter)
			--  _____  _    ___     _______ _____ _____  _____ 
			-- |  __ \| |  | \ \   / / ____|_   _/ ____|/ ____|
			-- | |__) | |__| |\ \_/ / (___   | || |    | (___  
			-- |  ___/|  __  | \   / \___ \  | || |     \___ \ 
			-- | |    | |  | |  | |  ____) |_| || |____ ____) |
			-- |_|    |_|  |_|  |_| |_____/|_____\_____|_____/ 
			if ent.isleft and not ent.isright then
				ent.body.vx = -ent.body.speed
			elseif ent.isright and not ent.isleft then
				ent.body.vx = ent.body.speed
			end
			if ent.isup and not ent.isdown then
				ent.body.vy = -ent.body.upspeed
			elseif ent.isdown and not ent.isup then
				ent.body.vy = ent.body.upspeed
			end
			-- move
			ent.pos = vector(nextx, nexty)
			ent.sprite:setPosition(ent.pos + vector(ent.collbox.w/2, -ent.h/2+ent.collbox.h))
		end
		Core.yield(1)
	end
	Core.asyncCall(fun)
end

What it does:

  • first we check if the player1 is in the door sensor by checking the door id and the player1 sensor id
  • then we check if the door needs a key to open or not
  • if the door needs a key, we check the player1 inventory for that key
  • when the player1 is in the door sensor, we open the door in the given direction
  • when the player1 goes outside the door sensor, we close the door
  • finally cBump uses the state the door is in and moves the door open or close

sSensor.lua

Here is the sensor System. It is used for doors but can work for anything.

Please create a file "sSensor.lua" in the "_S" folder and the code:

SSensor = Core.class()

function SSensor:init(xtiny, xbworld, xplayer1)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.bworld = xbworld -- cbump world
	self.player1 = xplayer1
end

function SSensor:filter(ent) -- tiny function
	return ent.issensor
end

function SSensor:onAdd(ent) -- tiny function
--	print("SSensor:onAdd")
end

function SSensor:onRemove(ent) -- tiny function
--	print("SSensor:onRemove")
end

local p1rangetoofarx = myappwidth*1 -- disable systems to save some CPU, magik XXX
local p1rangetoofary = myappheight*1 -- disable systems to save some CPU, magik XXX
function SSensor:process(ent, dt) -- tiny function
	local function fun()
		-- OUTSIDE VISIBLE RANGE
		if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or
			ent.pos.x < self.player1.pos.x - p1rangetoofarx) or
			(ent.pos.y > self.player1.pos.y + p1rangetoofary or
			ent.pos.y < self.player1.pos.y - p1rangetoofary) then
			ent.doanimate = false
			return
		end
		local function collisionfilter2(item) -- only one param: "item", return true, false or nil
			if item.isplayer1 then return true end
		end
		--local items, len = world:queryRect(l,t,w,h, filter)
		local items, len2 = self.bworld:queryRect(
			ent.pos.x, ent.pos.y, ent.w, ent.h, collisionfilter2
		)
		for i = 1, len2 do
			items[i].insensor = ent.eid -- add an extra var to player1
		end
		Core.yield(1)
	end
	Core.asyncCall(fun)
end

What it does:

  • it uses cBump queryRect function to check if the player1 is within the sensor boundaries
  • the sensor collision filter (collisionfilter2) is where we add the player1 as a potential candidate for collision detection with the sensor
  • when the player1 is "in" the sensor, we set the player1 insensor variable to the id of the sensor. We can then querry that variable in the game to trigger events

sMvpf.lua

A quick one, the moving platforms. Please create a file "sMvpf.lua" in the "_S" folder and the code:

SMvpf = Core.class()

function SMvpf:init(xtiny, xbworld)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.bworld = xbworld -- cbump world
end

function SMvpf:filter(ent) -- tiny function
	return ent.ismvpf -- both mvpf and ptmvpf
end

function SMvpf:onAdd(ent) -- tiny function
--	print("SMvpf:onAdd")
end

function SMvpf:onRemove(ent) -- tiny function
--	print("SMvpf:onRemove")
end

function SMvpf:process(ent, dt) -- tiny function
	local function fun()
		local function collisionfilter(item, other) -- "touch", "cross", "slide", "bounce"
			return "cross"
		end
		-- cbump
		local goalx = ent.pos.x + ent.body.vx * dt
		local goaly = ent.pos.y + ent.body.vy * dt
		local nextx, nexty, _collisions, _len = self.bworld:move(ent, goalx, goaly, collisionfilter)
		-- motion ai x
		if ent.pos.x >= ent.distance.startpos.x + ent.distance.dx then
			ent.isleft, ent.isright = true, false
		elseif ent.pos.x <= ent.distance.startpos.x then
			ent.isleft, ent.isright = false, true
		end
		-- motion ai y
		if ent.dir == "SE" then -- initial movement going down to the right
			if ent.pos.y >= ent.distance.startpos.y + ent.distance.dy then
				ent.isup, ent.isdown = true, false
			elseif ent.pos.y <= ent.distance.startpos.y then
				ent.isup, ent.isdown = false, true
			end
		else
			if ent.pos.y <= ent.distance.startpos.y - ent.distance.dy then
				ent.isup, ent.isdown = false, true
			elseif ent.pos.y >= ent.distance.startpos.y then
				ent.isup, ent.isdown = true, false
			end
		end
		-- movement vx
		if ent.isleft and not ent.isright then -- LEFT
			ent.flip = -1
			ent.body.vx = -ent.body.speed
		elseif ent.isright and not ent.isleft then -- RIGHT
			ent.flip = 1
			ent.body.vx = ent.body.speed
		end
		-- movement vy
		if ent.isup and not ent.isdown then -- UP
			ent.body.vy = -ent.body.upspeed
		elseif ent.isdown and not ent.isup then -- DOWN
			ent.body.vy = ent.body.upspeed
		end
		-- move & flip
		ent.pos = vector(nextx, nexty)
		ent.sprite:setPosition(ent.pos + vector(ent.collbox.w/2, -ent.h/2+ent.collbox.h))
		Core.yield(1)
	end
	Core.asyncCall(fun)
end

What it does:

  • we use cBump to move the platform in the physics world
  • we set the default platform collision to cross using collisionfilter
  • we check the position of the platform and set its direction (up, down, left, right) relative to the distance it can travel
  • we set the moving platform velocity (speed)
  • we move the platform body (cBump) along with the platform sprite



Next?

That was a big one but it needed to be done. The last systems we will add are for the collectibles, doors, moving platforms and a couple more 😉. Don't worry they should be pretty short!


Prev.: Tuto tiny-ecs 2d platformer Part 10 Collision System
Next: Tuto tiny-ecs 2d platformer Part 12 XXX


Tutorial - tiny-ecs 2d platformer