Difference between revisions of "Tuto tiny-ecs 2d platformer Part 9 Systems"

From GiderosMobile
(wip)
 
(wip)
Line 4: Line 4:
 
We have our entities, we have our components, now the systems. What is an ECS '''System'''?
 
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:
+
  A System is a wrapper around function callbacks for manipulating Entities. Systems are implemented as tables.
 
   
 
   
'''function system:update(dt)'''
+
  There are a few optional callbacks:
 
  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: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: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: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: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:onAddToWorld(world)''' Called when the System is added to the World, before any entities are added to the system.
Line 20: Line 19:
 
  '''Please see [[Tiny-ecs#System_functions]] for more information'''
 
  '''Please see [[Tiny-ecs#System_functions]] for more information'''
  
That looks scary but worry not we won't use all the callback functions :-)
+
Don't worry, we won't use all the callback functions :-)
 
 
To put it simple a '''System''' manipulates entities. Let's see our first '''System'''.
 
  
 
== sDrawable.lua ==
 
== sDrawable.lua ==
Please create a file "'''sDrawable.lua'''" in the '''"_S"''' folder and the code:
+
Our first System will add/remove sprites from layers. Please create a file "'''sDrawable.lua'''" in the '''"_S"''' folder and the code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 
SDrawable = Core.class()
 
SDrawable = Core.class()
Line 38: Line 35:
  
 
function SDrawable:onAdd(ent) -- tiny function
 
function SDrawable:onAdd(ent) -- tiny function
-- print("SDrawable:onAdd(ent)")
+
-- print("SDrawable:onAdd(ent)", ent.pos)
 
ent.spritelayer:addChild(ent.sprite)
 
ent.spritelayer:addChild(ent.sprite)
 
end
 
end
Line 44: Line 41:
 
function SDrawable:onRemove(ent) -- tiny function
 
function SDrawable:onRemove(ent) -- tiny function
 
-- print("SDrawable:onRemove(ent)")
 
-- print("SDrawable:onRemove(ent)")
 +
-- if ent.isplayer1 then return end
 +
if ent.ispersistent then return end
 
ent.spritelayer:removeChild(ent.sprite)
 
ent.spritelayer:removeChild(ent.sprite)
 
-- cleaning?
 
-- cleaning?
Line 55: Line 54:
 
* 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 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.
 
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.
Line 76: Line 75:
  
 
function SPlayer1Control:onAdd(ent) -- tiny function
 
function SPlayer1Control:onAdd(ent) -- tiny function
 +
-- listeners
 
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
 
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
 
if ent.currlives > 0 then
 
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = true end
+
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then -- left
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = true end
+
ent.isleft = true
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = true end
+
elseif e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = true end
+
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:
 
-- ACTIONS:
-- isactionpunch1, isactionpunch2, isactionjumppunch1,
+
if e.keyCode == g_keyaction1 then -- shoot
-- isactionkick1, isactionkick2, isactionjumpkick1,
+
if ent.shield.currtimer <= 0 then -- no shoot while shield active, you choose!
-- isactionjump1
+
ent.isaction1 = true
if e.keyCode == g_keyaction1 then
+
end
ent.animation.frame = 0
+
elseif e.keyCode == g_keyaction2 then -- shield
ent.isactionpunch1 = true
+
ent.isaction2 = true
elseif e.keyCode == g_keyaction2 then
+
elseif e.keyCode == g_keyaction3 then -- dash
ent.animation.frame = 0
+
if ent.body.currdashcooldown <= 0 then
ent.isactionkick1 = true
+
ent.isaction3 = 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
Line 106: Line 106:
 
self.player1inputlayer:addEventListener(Event.KEY_UP, function(e)
 
self.player1inputlayer:addEventListener(Event.KEY_UP, function(e)
 
if ent.currlives > 0 then
 
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = false end
+
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then -- left
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = false end
+
ent.isleft = false
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = false end
+
end
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = false end
+
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
-- if e.keyCode == g_keyaction1 then ent.isactionpunch1 = false end
+
ent.isright = false
-- if e.keyCode == g_keyaction2 then ent.isactionkick1 = false end
+
end
-- if e.keyCode == g_keyaction3 then 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)
 
end)
Line 123: Line 138:
 
* when the player1 Entity is added to tiny-ecs '''World''', the System registers KEY_DOWN and KEY_UP events
 
* 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.
+
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 ==
Line 130: Line 145:
 
SPlayer1 = Core.class()
 
SPlayer1 = Core.class()
  
function SPlayer1:init(xtiny, xcamera) -- tiny function
+
function SPlayer1:init(xtiny, xbump, xcamera) -- tiny function
 
self.tiny = xtiny -- ref so we can remove entities from tiny system
 
self.tiny = xtiny -- ref so we can remove entities from tiny system
 
self.tiny.processingSystem(self) -- called once on init and every update
 
self.tiny.processingSystem(self) -- called once on init and every update
 +
self.bworld = xbump
 
-- fx
 
-- fx
 
self.camera = xcamera -- camera shake
 
self.camera = xcamera -- camera shake
 +
self.camcurrzoom = self.camera:getZoom()
 
-- sfx
 
-- sfx
self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
+
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
self.channel = self.snd:play(0, false, true)
 
 
end
 
end
  
Line 148: Line 164:
  
 
function SPlayer1:onRemove(ent) -- tiny function
 
function SPlayer1:onRemove(ent) -- tiny function
 +
self.bworld:remove(ent) -- remove collision box from cbump world here!
 
end
 
end
  
local resetanim = true
 
 
function SPlayer1:process(ent, dt) -- tiny function
 
function SPlayer1:process(ent, dt) -- tiny function
-- hurt fx
+
if ent.isaction1 then -- shoot
if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) then
+
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.washurt -= 1
ent.animation.curranim = g_ANIM_HURT_R
+
ent.isdirty = false
if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
 
 
if ent.washurt <= 0 then
 
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1)
+
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
self.camera:setZoom(1) -- zoom
+
self.camera:setZoom(self.camcurrzoom) -- zoom
 +
elseif ent.washurt < ent.recovertimer*0.5 then
 +
ent.hitfx:setVisible(false)
 
end
 
end
elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 then
+
elseif ent.wasbadlyhurt > 0 and ent.currlives > 0 then -- lose 1 life
ent.hitfx:setVisible(false)
 
 
ent.wasbadlyhurt -= 1
 
ent.wasbadlyhurt -= 1
ent.animation.curranim = g_ANIM_LOSE1_R
+
ent.isdirty = false
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
 
if ent.wasbadlyhurt <= 0 then
 
ent.sprite:setColorTransform(1, 1, 1, 1)
 
ent.sprite:setColorTransform(1, 1, 1, 1)
self.camera:setZoom(1) -- zoom
+
self.camera:setZoom(self.camcurrzoom) -- zoom
resetanim = true
+
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
 
end
 
end
if ent.isdirty then -- hit
+
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 function map(v, minSrc, maxSrc, minDst, maxDst, clampValue)
 
local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
 
local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
 
return not clampValue and newV or clamp(newV, minDst><maxDst, minDst<>maxDst)
 
return not clampValue and newV or clamp(newV, minDst><maxDst, minDst<>maxDst)
 
end
 
end
self.channel = self.snd:play()
+
ent.sprite:setColorTransform(1, 1, 0, 2)
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
 
 
ent.hitfx:setVisible(true)
 
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.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
 
ent.spritelayer:addChild(ent.hitfx)
 
ent.spritelayer:addChild(ent.hitfx)
 
ent.currhealth -= ent.damage
 
ent.currhealth -= ent.damage
 +
-- hud
 
local hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
 
local hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
 
self.tiny.hudhealth:setWidth(hudhealthwidth)
 
self.tiny.hudhealth:setWidth(hudhealthwidth)
Line 196: Line 284:
 
end
 
end
 
ent.washurt = ent.recovertimer -- timer for a flash effect
 
ent.washurt = ent.recovertimer -- timer for a flash effect
ent.sprite:setColorTransform(2, 0, 0, 2) -- the flash effect (a bright red color)
+
ent.animation.curranim = g_ANIM_HURT_R
 +
ent.animation.frame = 0 -- start animation at frame 0
 
ent.isdirty = false
 
ent.isdirty = false
self.camera:shake(0.6, 16) -- (duration, distance), you choose
 
self.camera:setZoom(1.2) -- zoom
 
 
if ent.currhealth <= 0 then
 
if ent.currhealth <= 0 then
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
+
ent.sprite:setColorTransform(1, 0, 0, 2)
self.camera:shake(0.8, 64) -- (duration, distance), you choose
 
 
ent.currlives -= 1
 
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.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
 
for i = 1, ent.currlives do self.tiny.hudlives[i]:setVisible(true) end -- dirty but easy XXX
Line 213: Line 300:
 
if ent.currlives == 1 then self.tiny.hudlives[1]:setColor(0xff0000) end
 
if ent.currlives == 1 then self.tiny.hudlives[1]:setColor(0xff0000) end
 
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
 
end
 
end
if ent.currlives <= 0 then -- deaded
+
-- deaded
 +
if ent.currlives <= 0 then
 
-- stop all movements
 
-- stop all movements
 
ent.isleft = false
 
ent.isleft = false
Line 223: Line 315:
 
-- play dead sequence
 
-- play dead sequence
 
ent.isdirty = false
 
ent.isdirty = false
resetanim = false
+
ent.washurt = 0
ent.washurt = ent.recovertimer
+
ent.wasbadlyhurt = 0
ent.wasbadlyhurt = ent.recoverbadtimer
 
 
ent.animation.curranim = g_ANIM_LOSE1_R
 
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(255*0.5/255, 255*0.5/255, 255*0.5/255, 1)
+
ent.sprite:setColorTransform(1, 1, 1, 5)
self.camera:setZoom(1) -- zoom
+
self.camera:setZoom(self.camcurrzoom) -- zoom
ent.animation.bmp:setY(ent.animation.bmp:getY()-1)
+
ent.body.currmass = -0.15 -- soul leaves body!
if ent.animation.bmp:getY() < -200 then -- you choose
+
ent.isup = true
switchToScene(LevelX.new())
+
local timer = Timer.new(2000, 1)
end
+
timer:addEventListener(Event.TIMER_COMPLETE, function(e)
 +
ent.restart = true
 +
end)
 +
timer:start()
 
end
 
end
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the player1 being hit or killed:
+
This System deals with the player1 actions:
 
* runs once on init and '''every game loop''' (''process'')
 
* 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
 
* 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'')
 
* 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
 
* when the player1 is hit, we add a camera shake and play some sound
Line 250: Line 345:
 
SNmes = Core.class()
 
SNmes = Core.class()
  
local random = math.random
+
local random, atan2, cos, sin = math.random, math.atan2, math.cos, math.sin
  
function SNmes:init(xtiny, xbump) -- tiny function
+
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.tiny = xtiny -- class ref so we can remove entities from tiny world
self.tiny.processingSystem(self) -- called once on init and every update
 
 
self.bworld = xbump
 
self.bworld = xbump
 +
self.player1 = xplayer1
 
-- sfx
 
-- sfx
self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
+
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
self.channel = self.snd:play(0, false, true)
 
 
end
 
end
  
Line 266: Line 361:
  
 
function SNmes:onAdd(ent) -- tiny function
 
function SNmes:onAdd(ent) -- tiny function
ent.flip = math.random(100)
+
-- print("SNmes:onAdd")
 +
ent.flip = random(100)
 
if ent.flip > 50 then ent.flip = 1 else ent.flip = -1 end
 
if ent.flip > 50 then ent.flip = 1 else ent.flip = -1 end
 
ent.currlives = ent.totallives
 
ent.currlives = ent.totallives
Line 272: Line 368:
 
ent.washurt = 0
 
ent.washurt = 0
 
ent.wasbadlyhurt = 0
 
ent.wasbadlyhurt = 0
ent.isdead = false
 
 
ent.curractiontimer = ent.actiontimer
 
ent.curractiontimer = ent.actiontimer
ent.positionystart = 0
 
-- abilities
 
ent.abilities = {}
 
if ent.headhitboxattack1 then ent.abilities[#ent.abilities+1] = 1 end -- punch1
 
if ent.headhitboxattack2 then ent.abilities[#ent.abilities+1] = 2 end -- punch2
 
if ent.spinehitboxattack1 then ent.abilities[#ent.abilities+1] = 3 end -- kick1
 
if ent.spinehitboxattack2 then ent.abilities[#ent.abilities+1] = 4 end -- kick2
 
if ent.headhitboxjattack1 then ent.abilities[#ent.abilities+1] = 5 end -- jumppunch1
 
if ent.spinehitboxjattack1 then ent.abilities[#ent.abilities+1] = 6 end -- jumpkick1
 
-- for k, v in pairs(ent.abilities) do print(k, v) end
 
 
end
 
end
  
 
function SNmes:onRemove(ent) -- tiny function
 
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!
 
self.bworld:remove(ent) -- remove collision box from cbump world here!
 
end
 
end
  
local resetanim = true
 
 
function SNmes:process(ent, dt) -- tiny function
 
function SNmes:process(ent, dt) -- tiny function
-- hurt fx
+
if ent.isaction1 then -- shoot
if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) and not ent.isdead then
+
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.washurt -= 1
ent.animation.curranim = g_ANIM_HURT_R
+
ent.isdirty = false
if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
+
if ent.washurt <= 0 then
if ent.washurt <= 0 then ent.sprite:setColorTransform(1, 1, 1, 1) end
+
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 and not ent.isdead then
+
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.wasbadlyhurt -= 1
ent.animation.curranim = g_ANIM_LOSE1_R
+
ent.isdirty = false
if ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
+
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.hitfx:setVisible(false)
if resetanim then
 
resetanim = false
 
ent.animation.frame = 0
 
end
 
 
ent.animation.curranim = g_ANIM_STANDUP_R
 
ent.animation.curranim = g_ANIM_STANDUP_R
 +
ent.animation.frame = 0 -- start animation at frame 0
 
end
 
end
if ent.wasbadlyhurt <= 0 then
 
ent.sprite:setColorTransform(1, 1, 1, 1)
 
resetanim = true
 
end
 
 
end
 
end
if ent.isdirty then -- hit
+
if ent.body.currdashtimer > 0 then -- invicible while dashing
self.channel = self.snd:play()
+
ent.isdirty = false
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
+
end
 +
-- hit
 +
if ent.isdirty then
 +
ent.sprite:setColorTransform(0, 1, 0, 2)
 
ent.hitfx:setVisible(true)
 
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.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
 
ent.spritelayer:addChild(ent.hitfx)
 
ent.spritelayer:addChild(ent.hitfx)
 
ent.currhealth -= ent.damage
 
ent.currhealth -= ent.damage
 
ent.washurt = ent.recovertimer -- timer for a flash effect
 
ent.washurt = ent.recovertimer -- timer for a flash effect
-- ent.sprite:setColorTransform(0, 0, 2, 3) -- the flash effect (a bright red color)
+
ent.animation.curranim = g_ANIM_HURT_R
 +
ent.animation.frame = 0 -- start animation at frame 0
 
ent.isdirty = false
 
ent.isdirty = false
 
if ent.currhealth <= 0 then
 
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.wasbadlyhurt = ent.recoverbadtimer -- timer for actor to stand back up
ent.currlives -= 1
+
ent.animation.curranim = g_ANIM_LOSE1_R
if ent.currlives > 0 then ent.currhealth = ent.totalhealth end
 
 
end
 
end
 
end
 
end
if ent.currlives <= 0 then -- deaded
+
-- 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
 
-- stop all movements
 
ent.isleft = false
 
ent.isleft = false
Line 338: Line 525:
 
-- play dead sequence
 
-- play dead sequence
 
ent.isdirty = false
 
ent.isdirty = false
ent.washurt = ent.recovertimer
+
ent.washurt = 0 -- ent.recovertimer
ent.wasbadlyhurt = ent.recoverbadtimer
+
ent.wasbadlyhurt = 0 -- ent.recoverbadtimer
-- blood
+
if ent.readytoremove then
if not ent.isdead then
+
-- blood
 
ent.hitfx:setVisible(true)
 
ent.hitfx:setVisible(true)
ent.hitfx:setColorTransform(3, 0, 0, random(1, 3)/10) -- blood stain
+
ent.hitfx:setColorTransform(3, 0, 0, random(1, 3)*0.1) -- red color modulate
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2, ent.pos.y)
+
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:setRotation(random(360))
ent.hitfx:setScale(random(5, 8)/10)
+
ent.hitfx:setScale(random(5, 8)*0.1)
 
ent.bgfxlayer:addChild(ent.hitfx)
 
ent.bgfxlayer:addChild(ent.hitfx)
ent.isdead = true
+
ent.animation.curranim = g_ANIM_LOSE1_R
end
+
ent.sprite:setColorTransform(0.5, 0.5, 0.5, 0.5)
ent.animation.curranim = g_ANIM_LOSE1_R
 
resetanim = false -- ??? XXX
 
ent.sprite:setColorTransform((-ent.pos.y<>ent.pos.y)/255, (-ent.pos.y<>ent.pos.y)/255, 0, 1)
 
ent.shadow.sprite:setVisible(false)
 
ent.pos -= vector(8*ent.flip, 8)
 
ent.sprite:setPosition(ent.pos)
 
ent.sprite:setScale(ent.sprite:getScale()+0.07)
 
if ent.pos.y < -256 then
 
 
self.tiny.tworld:removeEntity(ent) -- sprite is removed in SDrawable
 
self.tiny.tworld:removeEntity(ent) -- sprite is removed in SDrawable
self.tiny.numberofnmes -= 1
 
 
end
 
end
 
end
 
end
Line 365: Line 543:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the enemies being hit or killed:
+
This System deals with the enemies actions:
 
* runs once on init and '''every game loop''' (''process'')
 
* runs once on init and '''every game loop''' (''process'')
 
* in ''init'' we add a sound to add some juice to the game
 
* in ''init'' we add a sound to add some juice to the game
 
* ''onAdd'' some explanation below
 
* ''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
 
* 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.
 
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.
  
The other thing worth noting is an enemy ability. Depending on the attacks an Entity has, they are stored in an ''abilities'' table. In a artificial intelligence System we will add shortly, the code will iterate the ''abilities'' table and pick a random attack an Entity can perform.
+
 
 +
 
 +
 
 +
 
 +
 
 +
 
 +
 
 +
 
 +
 
 +
 
 +
 
  
 
== sAI.lua ==
 
== sAI.lua ==

Revision as of 04:46, 5 November 2025

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