Tuto tiny-ecs 2d platformer 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. There are a few 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:process(entity, dt) Called once on init and every frames. *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
Don't worry, we won't use all the callback functions :-)
sDrawable.lua
Our first System will add/remove sprites from layers. 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.pos)
ent.spritelayer:addChild(ent.sprite)
end
function SDrawable:onRemove(ent) -- tiny function
-- print("SDrawable:onRemove(ent)")
-- if ent.isplayer1 then return end
if ent.ispersistent then return end
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 unless it has a ispersistent id
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
-- listeners
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then -- left
ent.isleft = true
elseif e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
ent.isright = true
end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then -- up
ent.isup = true
ent.wasup = false -- allows initial jump
ent.body.currinputbuffer = ent.body.inputbuffer
elseif e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then -- down
ent.isdown = true
end
-- ACTIONS:
if e.keyCode == g_keyaction1 then -- shoot
if ent.shield.currtimer <= 0 then -- no shoot while shield active, you choose!
ent.isaction1 = true
end
elseif e.keyCode == g_keyaction2 then -- shield
ent.isaction2 = true
elseif e.keyCode == g_keyaction3 then -- dash
if ent.body.currdashcooldown <= 0 then
ent.isaction3 = 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 -- left
ent.isleft = false
end
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
ent.isright = false
end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then -- up
ent.isup = false
ent.wasup = false -- prevent constant jumps
if ent.body.vy < ent.body.upspeed*0.5 then -- variable jump height
local function lerp(a,b,t) return a + (b-a) * t end
-- ent.body.vy = lerp(ent.body.vy, ent.body.upspeed*0.5, 0.5)
ent.body.vy = lerp(ent.body.vy, ent.body.upspeed*0.8333, 0.25)
end
end
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then -- down
ent.isdown = false
ent.wasdown = false -- prevent constant going down ptpf
end
if e.keyCode == g_keyaction1 then ent.isaction1 = false end
if e.keyCode == g_keyaction2 then ent.isaction2 = false end
if e.keyCode == g_keyaction3 then ent.isaction3 = false 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 collision System we will add.
sPlayer1.lua
"sPlayer1.lua" in the "_S" folder. The code:
SPlayer1 = Core.class()
function SPlayer1:init(xtiny, xbump, 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
self.bworld = xbump
-- fx
self.camera = xcamera -- camera shake
self.camcurrzoom = self.camera:getZoom()
-- sfx
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
end
function SPlayer1:filter(ent) -- tiny function
return ent.isplayer1
end
function SPlayer1:onAdd(ent) -- tiny function
end
function SPlayer1:onRemove(ent) -- tiny function
self.bworld:remove(ent) -- remove collision box from cbump world here!
end
function SPlayer1:process(ent, dt) -- tiny function
if ent.isaction1 then -- shoot
ent.isaction1 = false
local projectilespeed = 60*8 -- 54*8
local xangle = ^<0
if ent.flip == -1 then xangle = ^<180 end
local vx, vy = projectilespeed * math.cos(xangle), projectilespeed * math.sin(xangle)
--EProjectiles:init(xid, xmass, xangle, xspritelayer, xpos, xvx, xvy, dx, dy, xpersist)
local p = EProjectiles.new(
1, 0, xangle, ent.spritelayer,
ent.pos + vector(ent.collbox.w*0.5, ent.collbox.h*0.4),
vx, vy, 36*8, 40*8, false
)
p.body.vx = vx
p.body.vy = vy
self.tiny.tworld:addEntity(p)
self.bworld:add(p, p.pos.x, p.pos.y, p.collbox.w, p.collbox.h)
end
if ent.isaction2 then -- shield
ent.isaction2 = false
ent.shield.currtimer = ent.shield.timer
ent.shield.sprite:setVisible(true)
end
if ent.isaction3 and ent.body.candash then -- dash
ent.isaction3 = false
ent.body.currdashtimer = ent.body.dashtimer
ent.body.currdashcooldown = ent.body.dashcooldown
end
-- shield collision
if ent.shield.currtimer > 0 then
local function collisionfilter2(item) -- only one param: "item", return true, false or nil
if item.isnme or (item.isprojectile and item.eid > 1) then
return true
end
end
ent.shield.sprite:setScale(ent.shield.sprite.sx*ent.flip, ent.shield.sprite.sy)
ent.shield.sprite:setPosition(
ent.pos + vector(ent.collbox.w/2, 0) +
ent.shield.offset*vector(ent.shield.sprite.sx*ent.flip, ent.shield.sprite.sy)
)
local pw, ph = ent.shield.sprite:getWidth(), ent.shield.sprite:getHeight()
--local items, len = world:queryRect(l, t, w, h, filter)
local items, len2 = self.bworld:queryRect(
ent.pos.x+ent.shield.offset.x*ent.flip-pw*0.5+ent.collbox.w*0.5,
ent.pos.y+ent.shield.offset.y-ph*0.5,
pw, ph,
collisionfilter2
)
for i = 1, len2 do
local item = items[i]
if ent.shield.currtimer > 0 then
item.damage = ent.shield.damage
item.isdirty = true
end
end
end
if ent.shield and ent.shield.currtimer > 0 then
ent.shield.currtimer -= 1
if ent.shield.currtimer <= 0 then
ent.shield.sprite:setVisible(false)
end
end
-- hurt
if ent.washurt > 0 and ent.wasbadlyhurt <= 0 and ent.currlives > 0 then -- lose 1 nrg
ent.washurt -= 1
ent.isdirty = false
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
self.camera:setZoom(self.camcurrzoom) -- zoom
elseif ent.washurt < ent.recovertimer*0.5 then
ent.hitfx:setVisible(false)
end
elseif ent.wasbadlyhurt > 0 and ent.currlives > 0 then -- lose 1 life
ent.wasbadlyhurt -= 1
ent.isdirty = false
if ent.wasbadlyhurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1)
self.camera:setZoom(self.camcurrzoom) -- zoom
elseif ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
ent.hitfx:setVisible(false)
ent.animation.curranim = g_ANIM_STANDUP_R
if ent.iswallcontacts then
ent.animation.curranim = g_ANIM_WALL_IDLE_R
end
ent.animation.frame = 0 -- start animation at frame 0
end
end
if ent.body.currdashtimer > 0 then -- invicible while dashing
ent.isdirty = false
end
-- hit
if ent.isdirty then
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
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
ent.sprite:setColorTransform(1, 1, 0, 2)
ent.hitfx:setVisible(true)
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
ent.spritelayer:addChild(ent.hitfx)
ent.currhealth -= ent.damage
-- hud
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.animation.curranim = g_ANIM_HURT_R
ent.animation.frame = 0 -- start animation at frame 0
ent.isdirty = false
if ent.currhealth <= 0 then
ent.sprite:setColorTransform(1, 0, 0, 2)
ent.currlives -= 1
-- hud
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
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
ent.animation.curranim = g_ANIM_LOSE1_R
if ent.iswallcontacts then ent.animation.curranim = g_ANIM_HURT_R end
self.camera:shake(0.8, 64) -- (duration, distance), you choose
end
end
-- deaded
if ent.currlives <= 0 then
-- stop all movements
ent.isleft = false
ent.isright = false
ent.isup = false
ent.isdown = false
-- play dead sequence
ent.isdirty = false
ent.washurt = 0
ent.wasbadlyhurt = 0
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(1, 1, 1, 5)
self.camera:setZoom(self.camcurrzoom) -- zoom
ent.body.currmass = -0.15 -- soul leaves body!
ent.isup = true
local timer = Timer.new(2000, 1)
timer:addEventListener(Event.TIMER_COMPLETE, function(e)
ent.restart = true
end)
timer:start()
end
end
This System deals with the player1 actions:
- 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
- when an action button is pressed we execute it (action1 = shoot, action2 = shield, action3 = dash)
- 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
sNmes.lua
"sNmes.lua" in the "_S" folder. The code:
SNmes = Core.class()
local random, atan2, cos, sin = math.random, math.atan2, math.cos, math.sin
function SNmes:init(xtiny, xbump, xplayer1) -- tiny function
xtiny.processingSystem(self) -- called once on init and every update
self.tiny = xtiny -- class ref so we can remove entities from tiny world
self.bworld = xbump
self.player1 = xplayer1
-- sfx
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
end
function SNmes:filter(ent) -- tiny function
return ent.isnme
end
function SNmes:onAdd(ent) -- tiny function
-- print("SNmes:onAdd")
ent.flip = random(100)
if ent.flip > 50 then ent.flip = 1 else ent.flip = -1 end
ent.currlives = ent.totallives
ent.currhealth = ent.totalhealth
ent.washurt = 0
ent.wasbadlyhurt = 0
ent.curractiontimer = ent.actiontimer
end
function SNmes:onRemove(ent) -- tiny function
-- print("SNmes:onRemove")
local function fun()
ent.shield.sprite:setVisible(false)
ent.shield.sprite = nil
if ent.collectible then
if ent.collectible:find("door") then -- append "key" to eid
ent.collectible = "key"..tostring(ent.collectible)
end
--ECollectibles:init(xid, xspritelayer, xpos, xspeed, xdx, xdy)
local el = ECollectibles.new(
ent.collectible, ent.spritelayer,
ent.pos+vector(ent.collbox.w/4, 0*ent.collbox.h),
0.8*8, 6*8, 8*8
)
self.tiny.tworld:addEntity(el)
self.bworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
end
Core.yield(1)
end
Core.asyncCall(fun)
self.bworld:remove(ent) -- remove collision box from cbump world here!
end
function SNmes:process(ent, dt) -- tiny function
if ent.isaction1 then -- shoot
ent.isaction1 = false
local projectilespeed = 28*8
local xangle = atan2(self.player1.pos.y-ent.pos.y, self.player1.pos.x-ent.pos.x)
local offset = vector(ent.collbox.w*0.5, ent.collbox.h*0.4)
if ent.eid == 100 then -- shoot straight
if ent.flip == 1 then xangle = ^<0 -- shoot right
else xangle = ^<180 -- shoot left
end
projectilespeed = 26*8
end
local vx, vy = projectilespeed*cos(xangle), projectilespeed*sin(xangle)
--EProjectiles:init(xid, xmass, xangle, xspritelayer, xpos, xvx, xvy, dx, dy, xpersist)
local p = EProjectiles.new(
100, 0.1, xangle, ent.spritelayer, ent.pos+offset, vx, vy, 32*8, 32*8
)
p.body.vx = vx
p.body.vy = vy
self.tiny.tworld:addEntity(p)
self.bworld:add(p, p.pos.x, p.pos.y, p.collbox.w, p.collbox.h)
end
if ent.isaction2 then -- shield
ent.isaction2 = false
if ent.shield then
ent.shield.currtimer = ent.shield.timer
ent.shield.sprite:setVisible(true)
end
end
if ent.isaction3 then -- dash
ent.isaction3 = false
ent.body.currdashtimer = ent.body.dashtimer
ent.body.currdashcooldown = ent.body.dashcooldown
end
-- shield collision
if ent.shield and ent.currlives > 0 then
local function collisionfilter2(item) -- only one param: "item", return true, false or nil
if item.isplayer1 or (item.isprojectile and item.eid == 1) then
return true
end
end
local shiledsprite = ent.shield.sprite -- faster
shiledsprite:setScale(shiledsprite.sx*ent.flip, shiledsprite.sy)
shiledsprite:setPosition(
ent.pos +
vector(ent.collbox.w/2, 0) +
ent.shield.offset*vector(shiledsprite.sx*ent.flip, shiledsprite.sy)
)
local pw, ph = shiledsprite:getWidth(), shiledsprite:getHeight()
--local items, len = world:queryRect(l,t,w,h, filter)
local items, len2 = self.bworld:queryRect(
ent.pos.x+ent.shield.offset.x*ent.flip-pw*0.5+ent.collbox.w*0.5,
ent.pos.y+ent.shield.offset.y-ph*0.5,
pw, ph,
collisionfilter2)
for i = 1, len2 do
local item = items[i]
if shiledsprite:isVisible() then
item.damage = ent.shield.damage
item.isdirty = true
end
end
end
if ent.shield and ent.shield.currtimer > 0 then
ent.shield.currtimer -= 1
if ent.shield.currtimer <= 0 then
ent.shield.sprite:setVisible(false)
end
end
-- hurt
if ent.washurt > 0 and ent.wasbadlyhurt <= 0 and ent.currlives > 0 then -- lose 1 nrg
ent.washurt -= 1
ent.isdirty = false
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
elseif ent.washurt < ent.recovertimer*0.5 then
ent.hitfx:setVisible(false)
end
elseif ent.wasbadlyhurt > 0 and ent.currlives > 0 then -- lose 1 life
ent.wasbadlyhurt -= 1
ent.isdirty = false
if ent.wasbadlyhurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
elseif ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
ent.hitfx:setVisible(false)
ent.animation.curranim = g_ANIM_STANDUP_R
ent.animation.frame = 0 -- start animation at frame 0
end
end
if ent.body.currdashtimer > 0 then -- invicible while dashing
ent.isdirty = false
end
-- hit
if ent.isdirty then
ent.sprite:setColorTransform(0, 1, 0, 2)
ent.hitfx:setVisible(true)
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
ent.spritelayer:addChild(ent.hitfx)
ent.currhealth -= ent.damage
ent.washurt = ent.recovertimer -- timer for a flash effect
ent.animation.curranim = g_ANIM_HURT_R
ent.animation.frame = 0 -- start animation at frame 0
ent.isdirty = false
if ent.currhealth <= 0 then
ent.sprite:setColorTransform(0, 1, 1, 2)
ent.currlives -= 1
if ent.currlives > 0 then
ent.currhealth = ent.totalhealth
end
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for actor to stand back up
ent.animation.curranim = g_ANIM_LOSE1_R
end
end
-- deaded
if ent.currlives <= 0 then
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
-- stop all movements
ent.isleft = false
ent.isright = false
ent.isup = false
ent.isdown = false
-- play dead sequence
ent.isdirty = false
ent.washurt = 0 -- ent.recovertimer
ent.wasbadlyhurt = 0 -- ent.recoverbadtimer
if ent.readytoremove then
-- blood
ent.hitfx:setVisible(true)
ent.hitfx:setColorTransform(3, 0, 0, random(1, 3)*0.1) -- red color modulate
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y+ent.h*0.5)
ent.hitfx:setRotation(random(360))
ent.hitfx:setScale(random(5, 8)*0.1)
ent.bgfxlayer:addChild(ent.hitfx)
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(0.5, 0.5, 0.5, 0.5)
self.tiny.tworld:removeEntity(ent) -- sprite is removed in SDrawable
end
end
end
This System deals with the enemies actions:
- runs once on init and every game loop (process)
- in init we add a sound to add some juice to the game
- onAdd some explanation below
- when an action is triggered, we execute it (action1 = shoot, action2 = shield, action3 = dash). Actions will be triggered by an AI System
- when an enemy is hit, we play some sound
- onRemove the enemy is dead, we check if it 'holds' an item (key, coin, ...) and we add the item to the game
In the onAdd function it is worth noting that instead of creating all the variables for the enemies in their Entity code, I found it 'clever' to put them in the enemy System as they all share the same variables.
sAI.lua
"sAI.lua" in the "_S" folder. The code:
SAI = Core.class()
local random = math.random
function SAI:init(xtiny, xplayer1) -- tiny function
xtiny.processingSystem(self) -- called once on init and every update
self.player1 = xplayer1
end
function SAI:filter(ent) -- tiny function
return ent.ai
end
function SAI:onAdd(ent) -- tiny function
end
function SAI:onRemove(ent) -- tiny function
end
local p1rangex = 192 -- 192 -- magik XXX
local p1hitrangex = 64 -- magik XXX
local p1rangey = 96 -- 96 -- magik XXX
local p1hitrangey = 16 -- magik XXX
local rndaction = 0 -- random punch/kick action
local p1rangetoofar = myappwidth -- disable system to save some CPU, magik XXX
function SAI:process(ent, dt) -- tiny function
if ent.isdead then return end
local function fun()
-- some flags
ent.doanimate = true -- to save some cpu
ent.readytohit = false
if (ent.pos.x > self.player1.pos.x + p1rangetoofar or ent.pos.x < self.player1.pos.x - p1rangetoofar) then
ent.doanimate = false
return
end
if (ent.pos.x > self.player1.pos.x + p1rangex or ent.pos.x < self.player1.pos.x - p1rangex) or
(ent.pos.y > self.player1.pos.y + p1rangey or ent.pos.y < self.player1.pos.y - p1rangey) then -- OUTSIDE ATTACK RANGE
-- idle
ent.isleft, ent.isright = false, false
ent.isup, ent.isdown = false, false
ent.body.currspeed = ent.body.speed
ent.body.currjumpspeed = ent.body.jumpspeed
else -- INSIDE ATTACK RANGE
-- x
if ent.pos.x > random(self.player1.pos.x+p1hitrangex, self.player1.pos.x+p1rangex) then
ent.isleft, ent.isright = true, false
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
elseif ent.pos.x < random(self.player1.pos.x-p1rangex, self.player1.pos.x-p1hitrangex) then
ent.isleft, ent.isright = false, true
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
end
-- y
if ent.pos.y > random(self.player1.pos.y, self.player1.pos.y+p1hitrangey) then
ent.isup, ent.isdown = true, false
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
ent.readytohit = true
elseif ent.pos.y < random(self.player1.pos.y-p1hitrangey, self.player1.pos.y) then
ent.isup, ent.isdown = false, true
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
ent.readytohit = true
end
-- nmes always face player1
if not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
if ent.pos.x > self.player1.pos.x then ent.flip = -1
else ent.flip = 1
end
end
end
-- ATTACK
if ent.readytohit then
ent.curractiontimer -= 1
if ent.curractiontimer < 0 then
ent.animation.frame = 0
rndaction = ent.abilities[random(#ent.abilities)] -- pick a random attack
if rndaction == 1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.isactionpunch1 = true
elseif rndaction == 2 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.isactionpunch2 = true
elseif rndaction == 3 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.isactionkick1 = true
elseif rndaction == 4 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.isactionkick2 = true
elseif rndaction == 5 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.positionystart = ent.pos.y
ent.body.isonfloor = false
ent.body.isgoingup = true
ent.isactionjumppunch1 = true
ent.body.currspeed *= 1+random(4)*0.1 -- randomize speed, you choose to add it and the params
-- jump in the direction of the flip
if ent.flip == 1 then ent.isleft = false ent.isright = true
else ent.isleft = true ent.isright = false
end
elseif rndaction == 6 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
ent.positionystart = ent.pos.y
ent.body.isonfloor = false
ent.body.isgoingup = true
ent.isactionjumpkick1 = true
ent.body.currspeed *= 1+random(3)*0.1 -- randomize speed, you choose to add it and the params
-- jump in the direction of the flip
if ent.flip == 1 then ent.isleft = false ent.isright = true
else ent.isleft = true ent.isright = false
end
end
ent.curractiontimer = ent.actiontimer
end
end
Core.yield(1)
end
Core.asyncCall(fun) -- profiler seems to be faster without asyncCall (because of pairs traversing?)
end
This System controls all the entities with an artificial intelligence (ai) id. In this System an entity can be in an idle state, a move state or an attack state. Each states are applied relative to the distance between the Entity and the player1.
sShadow.lua
Let's quickly add the Shadow System. "sShadow.lua" in the "_S" folder. The code:
SShadow = Core.class()
function SShadow:init(xtiny) -- tiny function
xtiny.processingSystem(self) -- called once on init and every update
end
function SShadow:filter(ent) -- tiny function
return ent.shadow
end
function SShadow:onAdd(ent) -- tiny function
ent.spritelayer:addChildAt(ent.shadow.sprite, 1) -- add shadow behind ent
end
function SShadow:onRemove(ent) -- tiny function
ent.spritelayer:removeChild(ent.shadow.sprite)
end
function SShadow:process(ent, dt) -- tiny function
local function fun()
ent.shadow.sprite:setPosition(ent.pos+vector(ent.collbox.w/2, ent.collbox.h/2))
if ent.body and not ent.body.isonfloor then
ent.shadow.sprite:setPosition(ent.pos.x+ent.collbox.w/2, ent.positionystart+ent.collbox.h/2)
end
Core.yield(1)
end
Core.asyncCall(fun)
end
This System adds a shadow below an Entity. If the Entity is in a jump state, we update the shadow only on the x axis.
Next?
Systems are fairly straight forward and fairly short for what they can achieve.
We have covered the first set of systems, we have a couple more to add.
Prev.: Tuto tiny-ecs 2d platformer Part 8 more entities
Next: Tuto tiny-ecs 2d platformer Part 10 Systems 2
Tutorial - tiny-ecs 2d platformer