Difference between revisions of "Tuto tiny-ecs beatemup Part 9 Systems"

From GiderosMobile
(wip)
(wip)
Line 54: Line 54:
 
* runs only once when it is called
 
* runs only once when it is called
 
* affects only entities which have a spritelayer variable (id) '''and''' a sprite variable (id)
 
* 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 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
+
* when an Entity is removed from tiny-ecs '''World''', the '''System''' removes the Entity from its Sprite layer
  
Easy, let's continue.
+
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.
  
== eCollectible.lua ==
+
== sPlayer1Control.lua ==
"'''eCollectible.lua'''" in the '''"_E"''' folder. The code:
+
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:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 +
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
 
</syntaxhighlight>
 
</syntaxhighlight>
  
You get the idea ;-)
+
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:
 +
<syntaxhighlight lang="lua">
 +
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
 +
</syntaxhighlight>
  
'''We are done making all our entities!!!'''
+
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? ==
 
== Next? ==

Revision as of 21:27, 21 November 2024

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