Tuto tiny-ecs beatemup Part 9 Systems

From GiderosMobile
Revision as of 21:27, 21 November 2024 by MoKaLux (talk | contribs) (wip)

The Systems

We have our entities, we have our components, now the systems. What is an ECS System?

A System is a wrapper around function callbacks for manipulating Entities. Systems are implemented as tables that contain at least one method; an update function that takes parameters like so:

function system:update(dt)

There are also a few other optional callbacks:
*function system:filter(entity) Returns true if this System should include this Entity, otherwise should return false. If this isn't specified, no Entities are included in the System.
*function system:onAdd(entity) Called when an Entity is added to the System.
*function system:onRemove(entity) Called when an Entity is removed from the System.
*function system:onModify(dt) Called when the System is modified by adding or removing Entities from the System.
*function system:onAddToWorld(world) Called when the System is added to the World, before any entities are added to the system.
*function system:onRemoveFromWorld(world) Called when the System is removed from the world, after all Entities are removed from the System.
*function system:preWrap(dt) Called on each system before update is called on any system.
*function system:postWrap(dt) Called on each system in reverse order after update is called on each system.

Please see Tiny-ecs#System_functions for more information

That looks scary but worry not we won't use all the callback functions :-)

To put it simple a System manipulates entities. Let's see our first System.

sDrawable.lua

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

SDrawable = Core.class()

function SDrawable:init(xtiny) -- tiny function
	xtiny.system(self) -- called only once on init (no update)
end

function SDrawable:filter(ent) -- tiny function
	return ent.spritelayer and ent.sprite
end

function SDrawable:onAdd(ent) -- tiny function
--	print("SDrawable:onAdd(ent)")
	ent.spritelayer:addChild(ent.sprite)
end

function SDrawable:onRemove(ent) -- tiny function
--	print("SDrawable:onRemove(ent)")
	ent.spritelayer:removeChild(ent.sprite)
	-- cleaning?
	ent.sprite = nil
	ent = nil
end

What it does:

  • runs only once when it is called
  • affects only entities which have a spritelayer variable (id) and a sprite variable (id)
  • when an Entity is added to tiny-ecs World, the System adds the Entity to its Sprite layer
  • when an Entity is removed from tiny-ecs World, the System removes the Entity from its Sprite layer

In other words, the System adds an Entity to a Sprite layer when the Entity is added to tiny-ecs World, and removes it from that Sprite layer when the Entity is destroyed.

sPlayer1Control.lua

I am adding systems in an order that seems logical and helps in understanding the making of the game. The next System we add is the player1 controller.

"sPlayer1Control.lua" in the "_S" folder. The code:

SPlayer1Control = Core.class()

function SPlayer1Control:init(xtiny, xplayer1inputlayer) -- tiny function
	xtiny.system(self) -- called only once on init (no update)
	self.player1inputlayer = xplayer1inputlayer
end

function SPlayer1Control:filter(ent) -- tiny function
	return ent.isplayer1
end

function SPlayer1Control:onAdd(ent) -- tiny function
	self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
		if ent.currlives > 0 then
			if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = true end
			if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = true end
			if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = true end
			if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = true end
			-- ACTIONS:
			-- isactionpunch1, isactionpunch2, isactionjumppunch1,
			-- isactionkick1, isactionkick2, isactionjumpkick1,
			-- isactionjump1
			if e.keyCode == g_keyaction1 then
				ent.animation.frame = 0
				ent.isactionpunch1 = true
			elseif e.keyCode == g_keyaction2 then
				ent.animation.frame = 0
				ent.isactionkick1 = true
			end
			if e.keyCode == g_keyaction3 then
				if ent.body.isonfloor then
					ent.animation.frame = 0
					ent.positionystart = ent.pos.y
					ent.body.isonfloor = false
					ent.body.isgoingup = true
					ent.isactionjump1 = true
				end
			end
		end
	end)
	self.player1inputlayer:addEventListener(Event.KEY_UP, function(e)
		if ent.currlives > 0 then
			if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = false end
			if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = false end
			if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = false end
			if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = false end
--			if e.keyCode == g_keyaction1 then ent.isactionpunch1 = false end
--			if e.keyCode == g_keyaction2 then ent.isactionkick1 = false end
--			if e.keyCode == g_keyaction3 then end
		end
	end)
end

What it does:

  • runs only once when it is called
  • affects only entities with the isplayer1 id
  • when the player1 Entity is added to tiny-ecs World, the System registers KEY_DOWN and KEY_UP events

The System processes the user keys input and sets various flags to be processed in a future sDynamicBodies System we will add.

sPlayer1.lua

"sPlayer1.lua" in the "_S" folder. The code:

SPlayer1 = Core.class()

function SPlayer1:init(xtiny, xcamera) -- tiny function
	self.tiny = xtiny -- ref so we can remove entities from tiny system
	self.tiny.processingSystem(self) -- called once on init and every update
	-- fx
	self.camera = xcamera -- camera shake
	-- sfx
	self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
	self.channel = self.snd:play(0, false, true)
end

function SPlayer1:filter(ent) -- tiny function
	return ent.isplayer1
end

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

function SPlayer1:onRemove(ent) -- tiny function
end

local resetanim = true
function SPlayer1:process(ent, dt) -- tiny function
	-- hurt fx
	if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) then
		ent.washurt -= 1
		ent.animation.curranim = g_ANIM_HURT_R
		if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
		if ent.washurt <= 0 then
			ent.sprite:setColorTransform(1, 1, 1, 1)
			self.camera:setZoom(1) -- zoom
		end
	elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 then
		ent.hitfx:setVisible(false)
		ent.wasbadlyhurt -= 1
		ent.animation.curranim = g_ANIM_LOSE1_R
		if ent.wasbadlyhurt < ent.recoverbadtimer/2 then
			if resetanim then
				resetanim = false
				ent.animation.frame = 0
			end
			ent.animation.curranim = g_ANIM_STANDUP_R
		end
		if ent.wasbadlyhurt <= 0 then
			ent.sprite:setColorTransform(1, 1, 1, 1)
			self.camera:setZoom(1) -- zoom
			resetanim = true
		end
	end
	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
		self.channel = self.snd:play()
		if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
		ent.hitfx:setVisible(true)
		ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2+(ent.headhurtbox.x*ent.flip), ent.pos.y+ent.headhurtbox.y-ent.headhurtbox.h/2)
		ent.spritelayer:addChild(ent.hitfx)
		ent.currhealth -= ent.damage
		local hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
		self.tiny.hudhealth:setWidth(hudhealthwidth)
		if ent.currhealth < ent.totalhealth/3 then self.tiny.hudhealth:setColor(0xff0000)
		elseif ent.currhealth < ent.totalhealth/2 then self.tiny.hudhealth:setColor(0xff5500)
		else self.tiny.hudhealth:setColor(0x00ff00)
		end
		ent.washurt = ent.recovertimer -- timer for a flash effect
		ent.sprite:setColorTransform(2, 0, 0, 2) -- the flash effect (a bright red color)
		ent.isdirty = false
		self.camera:shake(0.6, 16) -- (duration, distance), you choose
		self.camera:setZoom(1.2) -- zoom
		if ent.currhealth <= 0 then
			ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
			self.camera:shake(0.8, 64) -- (duration, distance), you choose
			ent.currlives -= 1
			for i = 1, ent.totallives do self.tiny.hudlives[i]:setVisible(false) end -- dirty but easy XXX
			for i = 1, ent.currlives do self.tiny.hudlives[i]:setVisible(true) end -- dirty but easy XXX
			if ent.currlives > 0 then
				ent.currhealth = ent.totalhealth
				hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
				self.tiny.hudhealth:setWidth(hudhealthwidth)
				self.tiny.hudhealth:setColor(0x00ff00)
				if ent.currlives == 1 then self.tiny.hudlives[1]:setColor(0xff0000) end
			end
		end
	end
	if ent.currlives <= 0 then -- deaded
		-- stop all movements
		ent.isleft = false
		ent.isright = false
		ent.isup = false
		ent.isdown = false
		-- play dead sequence
		ent.isdirty = false
		resetanim = false
		ent.washurt = ent.recovertimer
		ent.wasbadlyhurt = ent.recoverbadtimer
		ent.animation.curranim = g_ANIM_LOSE1_R
		ent.sprite:setColorTransform(255*0.5/255, 255*0.5/255, 255*0.5/255, 1)
		self.camera:setZoom(1) -- zoom
		ent.animation.bmp:setY(ent.animation.bmp:getY()-1)
		if ent.animation.bmp:getY() < -200 then -- you choose
			switchToScene(LevelX.new())
		end
	end
end

This System deals with the player1 being hit or killed:

  • runs once on init and every game loop (process)
  • in init we add the camera and a sound to add some juice to the game
  • there are 2 kind of hurt animations depending on the player1 health (washurt and wasbadlyhurt)
  • when the player1 is hit, we add a camera shake and play some sound
  • we update the HUD
  • when the player1 is dead we play a death sequence and restart the current level

Next?

The time has come to tackle the systems. I will try to make it easy :-)


Prev.: Tuto tiny-ecs beatemup Part 8 Breakables
Next: Tuto tiny-ecs beatemup Part 10 XXX


Tutorial - tiny-ecs beatemup