Tuto tiny-ecs beatemup Part 9 Systems
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