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

From GiderosMobile
(wip)
 
 
(2 intermediate revisions by the same user not shown)
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 ==
Line 392: Line 570:
  
 
function SAI:onAdd(ent) -- tiny function
 
function SAI:onAdd(ent) -- tiny function
 +
-- abilities
 +
-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
 +
-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
 +
-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
 +
-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
 +
ent.abilities = {}
 +
if ent.body.upspeed > 0 then
 +
ent.abilities[#ent.abilities+1] = 1 -- jump/climb up
 +
end
 +
if ent.eid == 200 then
 +
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
 +
if ent.body.speed > 0 then
 +
ent.abilities[#ent.abilities+1] = 30 -- action3, dash
 +
end
 +
elseif ent.eid == 300 then -- nme boss1
 +
ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
 +
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
 +
if ent.body.speed > 0 then
 +
ent.abilities[#ent.abilities+1] = 30 -- action3, dash
 +
end
 +
else
 +
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
 +
end
 +
-- for k, v in pairs(ent.abilities) do print(k, v) end
 
end
 
end
  
Line 397: Line 599:
 
end
 
end
  
local p1rangex = 192 -- 192 -- magik XXX
+
local p1rangetoofarx = myappwidth*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1hitrangex = 64 -- magik XXX
+
local p1rangetoofary = myappheight*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1rangey = 96 -- 96 -- magik XXX
+
local p1outofrangex = myappwidth*1 -- 0.5, magik XXX
local p1hitrangey = 16 -- magik XXX
+
local p1outofrangey = myappheight*1 -- 0.5, magik XXX
local rndaction = 0 -- random punch/kick action
+
local p1actionrange = myappwidth*0.2 -- 3*8, 1*8, myappwidth*0.1, magik XXX
local p1rangetoofar = myappwidth -- disable system to save some CPU, magik XXX
 
 
function SAI:process(ent, dt) -- tiny function
 
function SAI:process(ent, dt) -- tiny function
if ent.isdead then return end
+
if ent.currlives <= 0 then
 +
return
 +
end
 
local function fun()
 
local function fun()
 
-- some flags
 
-- some flags
 
ent.doanimate = true -- to save some cpu
 
ent.doanimate = true -- to save some cpu
ent.readytohit = false
+
-- OUTSIDE VISIBLE RANGE
if (ent.pos.x > self.player1.pos.x + p1rangetoofar or ent.pos.x < self.player1.pos.x - p1rangetoofar) then
+
if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or
 +
ent.pos.x < self.player1.pos.x - p1rangetoofarx) or
 +
(ent.pos.y > self.player1.pos.y + p1rangetoofary or
 +
ent.pos.y < self.player1.pos.y - p1rangetoofary) then
 
ent.doanimate = false
 
ent.doanimate = false
 
return
 
return
 
end
 
end
if (ent.pos.x > self.player1.pos.x + p1rangex or ent.pos.x < self.player1.pos.x - p1rangex) or
+
-- OUTSIDE ACTION RANGE
(ent.pos.y > self.player1.pos.y + p1rangey or ent.pos.y < self.player1.pos.y - p1rangey) then -- OUTSIDE ATTACK RANGE
+
if (ent.pos.x > self.player1.pos.x+p1outofrangex or
 +
ent.pos.x < self.player1.pos.x-p1outofrangex) or
 +
(ent.pos.y > self.player1.pos.y+p1outofrangey or
 +
ent.pos.y < self.player1.pos.y-p1outofrangey) then
 
-- idle
 
-- idle
 
ent.isleft, ent.isright = false, false
 
ent.isleft, ent.isright = false, false
 
ent.isup, ent.isdown = false, false
 
ent.isup, ent.isdown = false, false
 
ent.body.currspeed = ent.body.speed
 
ent.body.currspeed = ent.body.speed
ent.body.currjumpspeed = ent.body.jumpspeed
+
ent.body.currupspeed = ent.body.upspeed
else -- INSIDE ATTACK RANGE
+
ent.readyforaction = false
 +
else -- INSIDE ACTION RANGE
 
-- x
 
-- x
if ent.pos.x > random(self.player1.pos.x+p1hitrangex, self.player1.pos.x+p1rangex) then
+
local rnd = random(100)
ent.isleft, ent.isright = true, false
+
if rnd > 1 and ent.body.speed > 0 then -- allow nme to stop walking, magik XXX
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
+
if ent.pos.x > random(self.player1.pos.x+p1actionrange, self.player1.pos.x+p1outofrangex) then
elseif ent.pos.x < random(self.player1.pos.x-p1rangex, self.player1.pos.x-p1hitrangex) then
+
ent.isleft, ent.isright = true, false
ent.isleft, ent.isright = false, true
+
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
+
elseif ent.pos.x < random(self.player1.pos.x-p1outofrangex, self.player1.pos.x-p1actionrange) then
 +
ent.isleft, ent.isright = false, true
 +
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
 +
end
 +
elseif ent.body.speed == 0 then -- nme always faces player1
 +
if ent.pos.x > self.player1.pos.x then
 +
ent.isleft, ent.isright = true, false
 +
else
 +
ent.isleft, ent.isright = false, true
 +
end
 +
else -- stop moving
 +
ent.isleft, ent.isright = false, false
 +
ent.body.currspeed = ent.body.speed
 +
end
 +
-- no going left or right on stairs/ladders
 +
if ent.isladdercontacts and not (ent.isfloorcontacts or ent.isptpfcontacts) then
 +
ent.isright, ent.isleft = false, false
 +
end
 +
-- bumping on a wall unless isleftofplayer
 +
ent.isleftofplayer = false
 +
if ent.pos.x < self.player1.pos.x then
 +
ent.isleftofplayer = true
 
end
 
end
 
-- y
 
-- y
if ent.pos.y > random(self.player1.pos.y, self.player1.pos.y+p1hitrangey) then
+
-- impulse on top of stairs/ladders and actor is going up
 +
if ent.isladdercontacts and ent.isptpfcontacts and ent.body.vy < 0 then
 
ent.isup, ent.isdown = true, false
 
ent.isup, ent.isdown = true, false
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
+
ent.wasup = false
ent.readytohit = true
+
ent.body.currinputbuffer = ent.body.inputbuffer
elseif ent.pos.y < random(self.player1.pos.y-p1hitrangey, self.player1.pos.y) then
+
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
ent.isup, ent.isdown = false, true
 
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
 
ent.readytohit = true
 
 
end
 
end
-- nmes always face player1
+
-- only act if entity has abilities
if not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
if #ent.abilities > 0 then
if ent.pos.x > self.player1.pos.x then ent.flip = -1
+
ent.readyforaction = true
else ent.flip = 1
 
end
 
 
end
 
end
 
end
 
end
-- ATTACK
+
-- ACTION
if ent.readytohit then
+
if ent.readyforaction then
 
ent.curractiontimer -= 1
 
ent.curractiontimer -= 1
 +
-- don't let the entity get stuck in place when in range
 +
if ent.body.vx == 0 and ent.body.speed > 0 then
 +
if ent.pos.x > self.player1.pos.x then ent.isleft, ent.isright = true, false
 +
else ent.isleft, ent.isright = false, true
 +
end
 +
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
 +
end
 
if ent.curractiontimer < 0 then
 
if ent.curractiontimer < 0 then
ent.animation.frame = 0
+
-- actor above player1, go down
rndaction = ent.abilities[random(#ent.abilities)] -- pick a random attack
+
if ent.pos.y < self.player1.pos.y and ent.body.upspeed > 0 then
if rndaction == 1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then  
+
if ent.isptpfcontacts then
ent.isactionpunch1 = true
+
local rnd = random(100)
elseif rndaction == 2 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
if rnd > 30 then -- 80, magik XXX
ent.isactionpunch2 = true
+
ent.isup, ent.isdown = false, true
elseif rndaction == 3 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
ent.wasdown = false
ent.isactionkick1 = true
+
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
elseif rndaction == 4 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
end
ent.isactionkick2 = true
+
elseif ent.body.upspeed > 0 then -- ladders/stairs
elseif rndaction == 5 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
ent.isup, ent.isdown = false, true
ent.positionystart = ent.pos.y
+
ent.wasdown = false
ent.body.isonfloor = false
+
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
ent.body.isgoingup = true
+
end
ent.isactionjumppunch1 = true
+
end
ent.body.currspeed *= 1+random(4)*0.1 -- randomize speed, you choose to add it and the params
+
-- actor below player1, jump
-- jump in the direction of the flip
+
if ent.pos.y > self.player1.pos.y + 8 and ent.body.upspeed > 0 then
if ent.flip == 1 then ent.isleft = false ent.isright = true
+
local rnd = random(100)
else ent.isleft = true ent.isright = false
+
if rnd > 40 then -- 80, magik XXX
 +
ent.isup, ent.isdown = true, false
 +
ent.wasup = false
 +
ent.body.currinputbuffer = ent.body.inputbuffer
 +
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
 
end
 
end
elseif rndaction == 6 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
end
ent.positionystart = ent.pos.y
+
-- pick a random available action
ent.body.isonfloor = false
+
local rndaction = ent.abilities[random(#ent.abilities)]
ent.body.isgoingup = true
+
-- movements
ent.isactionjumpkick1 = true
+
if rndaction == 1 and ent.body.upspeed > 0 then -- jump
ent.body.currspeed *= 1+random(3)*0.1 -- randomize speed, you choose to add it and the params
+
local rnd = random(100)
-- jump in the direction of the flip
+
if rnd > 70 then -- 80, jump
if ent.flip == 1 then ent.isleft = false ent.isright = true
+
ent.isup, ent.isdown = true, false
else ent.isleft = true ent.isright = false
+
ent.wasup = false
 +
ent.body.currinputbuffer = ent.body.inputbuffer
 +
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
 
end
 
end
 
end
 
end
ent.curractiontimer = ent.actiontimer
+
-- actions
 +
local rnd = random(100)
 +
if rndaction == 10 then -- shoot
 +
ent.isaction1 = false
 +
if rnd > 99 then -- rate
 +
ent.isaction1 = true
 +
end
 +
elseif rndaction == 20 then -- shield
 +
ent.isaction2 = false
 +
if rnd > 98 then -- rate
 +
ent.isaction2 = true
 +
end
 +
elseif rndaction == 30 then -- dash
 +
ent.isaction3 = false
 +
if rnd > 95 then -- rate
 +
ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
 +
ent.isaction3 = true
 +
end
 +
end
 +
end
 +
-- extra attack on jumping peak
 +
if ent.body.vy > 0 and ent.body.vy < 30 and ent.body.upspeed > 0 then -- magik XXX
 +
if ent.curractiontimer < 0 then
 +
local rndaction = ent.abilities[random(#ent.abilities)]
 +
local rnd = random(100)
 +
if rnd > 70 then -- rate
 +
if rndaction == 10 then -- shoot
 +
ent.isaction1 = true
 +
elseif rndaction == 20 then -- shield
 +
ent.isaction2 = true
 +
end
 +
else -- dash/shield
 +
local rnd2 = random(100)
 +
if rnd2 > 30 then
 +
if rndaction == 30 then -- dash
 +
ent.isaction3 = true
 +
end
 +
else
 +
ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
 +
if rndaction == 20 then -- shield
 +
ent.isaction2 = true
 +
end
 +
end
 +
end
 +
ent.curractiontimer = ent.actiontimer
 +
end
 
end
 
end
 
end
 
end
Line 490: Line 775:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
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''.
+
This System controls all the entities with an artificial intelligence (''ai'') id:
 +
* runs once on init and every game loop (process)
 +
* '''onAdd()''' we assign various abilities depending on the type of enemy id
 +
<syntaxhighlight lang="lua">
 +
-- abilities
 +
ent.abilities = {}
 +
-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
 +
-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
 +
-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
 +
-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
 +
</syntaxhighlight>
 +
* '''process()''' when the enemy is outside the visible screen we don't process it
 +
* '''process()''' when the enemy is in action range with the player1 we randomly trigger an action (ability)
  
== sShadow.lua ==
+
== sAnimation.lua ==
Let's quickly add the Shadow System. "'''sShadow.lua'''" in the '''"_S"''' folder. The code:
+
The animation System. "'''sAnimation.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
SShadow = Core.class()
+
SAnimation = Core.class()
  
function SShadow:init(xtiny) -- tiny function
+
function SAnimation:init(xtiny)
xtiny.processingSystem(self) -- called once on init and every update
+
xtiny.processingSystem(self) -- called once on init and every frames
 +
self.sndstepgrass = { sound=Sound.new("audio/sfx/footstep/Grass02.wav"), time=0, delay=0.2, }
 
end
 
end
  
function SShadow:filter(ent) -- tiny function
+
function SAnimation:filter(ent) -- tiny function
return ent.shadow
+
return ent.animation
 
end
 
end
  
function SShadow:onAdd(ent) -- tiny function
+
function SAnimation:onAdd(ent) -- tiny function
ent.spritelayer:addChildAt(ent.shadow.sprite, 1) -- add shadow behind ent
 
 
end
 
end
  
function SShadow:onRemove(ent) -- tiny function
+
function SAnimation:onRemove(ent) -- tiny function
ent.spritelayer:removeChild(ent.shadow.sprite)
+
ent.animation = nil -- free some memory?
 
end
 
end
  
function SShadow:process(ent, dt) -- tiny function
+
local checkanim
local function fun()
+
--@native -- new in Gideros
ent.shadow.sprite:setPosition(ent.pos+vector(ent.collbox.w/2, ent.collbox.h/2))
+
function SAnimation:process(ent, dt) -- tiny function
if ent.body and not ent.body.isonfloor then
+
-- a little boost?
ent.shadow.sprite:setPosition(ent.pos.x+ent.collbox.w/2, ent.positionystart+ent.collbox.h/2)
+
local anim = ent.animation
 +
 
 +
-- checkanim = anim.curranim -- if you are sure all animations are set else use below ternary operator code
 +
-- luau ternary operator (no end at the end), it's a 1 liner and seems fast?
 +
checkanim = if anim.anims[anim.curranim] then anim.curranim else g_ANIM_DEFAULT
 +
-- print("checkanim", checkanim)
 +
-- print("#anim.anims[checkanim]", #anim.anims[checkanim])
 +
 
 +
if not ent.doanimate then return end
 +
 
 +
anim.animtimer -= dt
 +
if anim.animtimer < 0 then
 +
anim.frame += 1
 +
anim.animtimer = anim.animspeed
 +
if checkanim == g_ANIM_DEFAULT then
 +
if anim.frame > #anim.anims[checkanim] then
 +
anim.frame = 1
 +
end
 +
elseif checkanim == g_ANIM_HURT_R or checkanim == g_ANIM_LOSE1_R or checkanim == g_ANIM_STANDUP_R then
 +
if anim.frame >= #anim.anims[checkanim] then
 +
anim.frame = #anim.anims[checkanim]
 +
end
 +
elseif checkanim == g_ANIM_JUMPUP_R or checkanim == g_ANIM_JUMPDOWN_R then
 +
if anim.frame > #anim.anims[checkanim] then
 +
anim.frame = #anim.anims[checkanim]
 +
end
 +
else -- any looping animation
 +
-- player1 steps sound fx
 +
if ent.isplayer1 then
 +
if (anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
 +
(anim.frame == 3 or anim.frame == 7) then
 +
local snd = self.sndstepgrass
 +
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
 +
end
 +
end
 +
-- loop animations
 +
if anim.frame > #anim.anims[checkanim] then
 +
anim.frame = 1
 +
end
 
end
 
end
Core.yield(1)
+
anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame])
 
end
 
end
Core.asyncCall(fun)
 
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
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.
+
This is the System responsible for all the animations in the game:
 +
* runs once on init and every game loop (process)
 +
* it executes an animation or falls back to the default animation ('''g_ANIM_DEFAULT''')
 +
* it only runs when the ''doanimate'' flag is set to true
 +
* then it increases the frame based on a timer
 +
* on certain animations, the frame loops back to 1
 +
* on other animations, the frame will stop at the end of the animation
 +
* we can also trigger events on specific frame number (eg. play sound)
  
 
== Next? ==
 
== Next? ==
Systems are fairly straight forward and fairly short for what they can achieve.
+
Systems are fairly straight forward and fairly short for what they achieve.
  
We have covered the first set of systems, we have a couple more to add.
+
We have covered the first set of systems, the next System is the heart of the game: the collision System.
  
  
 
Prev.: [[Tuto tiny-ecs 2d platformer Part 8 more entities]]</br>
 
Prev.: [[Tuto tiny-ecs 2d platformer Part 8 more entities]]</br>
'''Next: [[Tuto tiny-ecs 2d platformer Part 10 Systems 2]]'''
+
'''Next: [[Tuto tiny-ecs 2d platformer Part 10 Collision System]]'''
  
  
 
'''[[Tutorial - tiny-ecs 2d platformer]]'''
 
'''[[Tutorial - tiny-ecs 2d platformer]]'''
 
{{GIDEROS IMPORTANT LINKS}}
 
{{GIDEROS IMPORTANT LINKS}}

Latest revision as of 03:56, 6 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
	-- abilities
	-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
	-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
	-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
	-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
	ent.abilities = {}
	if ent.body.upspeed > 0 then
		ent.abilities[#ent.abilities+1] = 1 -- jump/climb up
	end
	if ent.eid == 200 then
		ent.abilities[#ent.abilities+1] = 20 -- action2, shield
		if ent.body.speed > 0 then
			ent.abilities[#ent.abilities+1] = 30 -- action3, dash
		end
	elseif ent.eid == 300 then -- nme boss1
		ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
		ent.abilities[#ent.abilities+1] = 20 -- action2, shield
		if ent.body.speed > 0 then
			ent.abilities[#ent.abilities+1] = 30 -- action3, dash
		end
	else
		ent.abilities[#ent.abilities+1] = 20 -- action2, shield
	end
--	for k, v in pairs(ent.abilities) do print(k, v) end
end

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

local p1rangetoofarx = myappwidth*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1rangetoofary = myappheight*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1outofrangex = myappwidth*1 -- 0.5, magik XXX
local p1outofrangey = myappheight*1 -- 0.5, magik XXX
local p1actionrange = myappwidth*0.2 -- 3*8, 1*8, myappwidth*0.1, magik XXX
function SAI:process(ent, dt) -- tiny function
	if ent.currlives <= 0 then
		return
	end
	local function fun()
		-- some flags
		ent.doanimate = true -- to save some cpu
		-- OUTSIDE VISIBLE RANGE
		if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or
			ent.pos.x < self.player1.pos.x - p1rangetoofarx) or
			(ent.pos.y > self.player1.pos.y + p1rangetoofary or
			ent.pos.y < self.player1.pos.y - p1rangetoofary) then
			ent.doanimate = false
			return
		end
		-- OUTSIDE ACTION RANGE
		if (ent.pos.x > self.player1.pos.x+p1outofrangex or
			ent.pos.x < self.player1.pos.x-p1outofrangex) or
			(ent.pos.y > self.player1.pos.y+p1outofrangey or
			ent.pos.y < self.player1.pos.y-p1outofrangey) then
			-- idle
			ent.isleft, ent.isright = false, false
			ent.isup, ent.isdown = false, false
			ent.body.currspeed = ent.body.speed
			ent.body.currupspeed = ent.body.upspeed
			ent.readyforaction = false
		else -- INSIDE ACTION RANGE
			-- x
			local rnd = random(100)
			if rnd > 1 and ent.body.speed > 0 then -- allow nme to stop walking, magik XXX
				if ent.pos.x > random(self.player1.pos.x+p1actionrange, self.player1.pos.x+p1outofrangex) then
					ent.isleft, ent.isright = true, false
					ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
				elseif ent.pos.x < random(self.player1.pos.x-p1outofrangex, self.player1.pos.x-p1actionrange) then
					ent.isleft, ent.isright = false, true
					ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
				end
			elseif ent.body.speed == 0 then -- nme always faces player1
				if ent.pos.x > self.player1.pos.x then
					ent.isleft, ent.isright = true, false
				else
					ent.isleft, ent.isright = false, true
				end
			else -- stop moving
				ent.isleft, ent.isright = false, false
				ent.body.currspeed = ent.body.speed
			end
			-- no going left or right on stairs/ladders
			if ent.isladdercontacts and not (ent.isfloorcontacts or ent.isptpfcontacts) then
				ent.isright, ent.isleft = false, false
			end
			-- bumping on a wall unless isleftofplayer
			ent.isleftofplayer = false
			if ent.pos.x < self.player1.pos.x then
				ent.isleftofplayer = true
			end
			-- y
			-- impulse on top of stairs/ladders and actor is going up
			if ent.isladdercontacts and ent.isptpfcontacts and ent.body.vy < 0 then
				ent.isup, ent.isdown = true, false
				ent.wasup = false
				ent.body.currinputbuffer = ent.body.inputbuffer
				ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
			end
			-- only act if entity has abilities
			if #ent.abilities > 0 then
				ent.readyforaction = true
			end
		end
		-- ACTION
		if ent.readyforaction then
			ent.curractiontimer -= 1
			-- don't let the entity get stuck in place when in range
			if ent.body.vx == 0 and ent.body.speed > 0 then
				if ent.pos.x > self.player1.pos.x then ent.isleft, ent.isright = true, false
				else ent.isleft, ent.isright = false, true
				end
				ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
			end
			if ent.curractiontimer < 0 then
				-- actor above player1, go down
				if ent.pos.y < self.player1.pos.y and ent.body.upspeed > 0 then
					if ent.isptpfcontacts then
						local rnd = random(100)
						if rnd > 30 then -- 80, magik XXX
							ent.isup, ent.isdown = false, true
							ent.wasdown = false
							ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
						end
					elseif ent.body.upspeed > 0 then -- ladders/stairs
						ent.isup, ent.isdown = false, true
						ent.wasdown = false
						ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
					end
				end
				-- actor below player1, jump
				if ent.pos.y > self.player1.pos.y + 8 and ent.body.upspeed > 0 then
					local rnd = random(100)
					if rnd > 40 then -- 80, magik XXX
						ent.isup, ent.isdown = true, false
						ent.wasup = false
						ent.body.currinputbuffer = ent.body.inputbuffer
						ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
					end
				end
				-- pick a random available action
				local rndaction = ent.abilities[random(#ent.abilities)]
				-- movements
				if rndaction == 1 and ent.body.upspeed > 0 then -- jump
					local rnd = random(100)
					if rnd > 70 then -- 80, jump
						ent.isup, ent.isdown = true, false
						ent.wasup = false
						ent.body.currinputbuffer = ent.body.inputbuffer
						ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
					end
				end
				-- actions
				local rnd = random(100)
				if rndaction == 10 then -- shoot
					ent.isaction1 = false
					if rnd > 99 then -- rate
						ent.isaction1 = true
					end
				elseif rndaction == 20 then -- shield
					ent.isaction2 = false
					if rnd > 98 then -- rate
						ent.isaction2 = true
					end
				elseif rndaction == 30 then -- dash
					ent.isaction3 = false
					if rnd > 95 then -- rate
						ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
						ent.isaction3 = true
					end
				end
			end
			-- extra attack on jumping peak
			if ent.body.vy > 0 and ent.body.vy < 30 and ent.body.upspeed > 0 then -- magik XXX
				if ent.curractiontimer < 0 then
					local rndaction = ent.abilities[random(#ent.abilities)]
					local rnd = random(100)
					if rnd > 70 then -- rate
						if rndaction == 10 then -- shoot
							ent.isaction1 = true
						elseif rndaction == 20 then -- shield
							ent.isaction2 = true
						end
					else -- dash/shield
						local rnd2 = random(100)
						if rnd2 > 30 then
							if rndaction == 30 then -- dash
								ent.isaction3 = true
							end
						else
							ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
							if rndaction == 20 then -- shield
								ent.isaction2 = true
							end
						end
					end
					ent.curractiontimer = ent.actiontimer
				end
			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:

  • runs once on init and every game loop (process)
  • onAdd() we assign various abilities depending on the type of enemy id
	-- abilities
	ent.abilities = {}
	-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
	-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
	-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
	-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
  • process() when the enemy is outside the visible screen we don't process it
  • process() when the enemy is in action range with the player1 we randomly trigger an action (ability)

sAnimation.lua

The animation System. "sAnimation.lua" in the "_S" folder. The code:

SAnimation = Core.class()

function SAnimation:init(xtiny)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.sndstepgrass = { sound=Sound.new("audio/sfx/footstep/Grass02.wav"), time=0, delay=0.2, }
end

function SAnimation:filter(ent) -- tiny function
	return ent.animation
end

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

function SAnimation:onRemove(ent) -- tiny function
	ent.animation = nil -- free some memory?
end

local checkanim
--@native -- new in Gideros
function SAnimation:process(ent, dt) -- tiny function
	-- a little boost?
	local anim = ent.animation

--	checkanim = anim.curranim -- if you are sure all animations are set else use below ternary operator code
	-- luau ternary operator (no end at the end), it's a 1 liner and seems fast?
	checkanim = if anim.anims[anim.curranim] then anim.curranim else g_ANIM_DEFAULT
--	print("checkanim", checkanim)
--	print("#anim.anims[checkanim]", #anim.anims[checkanim])

	if not ent.doanimate then return end

	anim.animtimer -= dt
	if anim.animtimer < 0 then
		anim.frame += 1
		anim.animtimer = anim.animspeed
		if checkanim == g_ANIM_DEFAULT then
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = 1
			end
		elseif checkanim == g_ANIM_HURT_R or checkanim == g_ANIM_LOSE1_R or checkanim == g_ANIM_STANDUP_R then
			if anim.frame >= #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
			end
		elseif checkanim == g_ANIM_JUMPUP_R or checkanim == g_ANIM_JUMPDOWN_R then
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
			end
		else -- any looping animation
			-- player1 steps sound fx
			if ent.isplayer1 then
				if (anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
					(anim.frame == 3 or anim.frame == 7) then
					local snd = self.sndstepgrass
					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
				end
			end
			-- loop animations
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = 1
			end
		end
		anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame])
	end
end

This is the System responsible for all the animations in the game:

  • runs once on init and every game loop (process)
  • it executes an animation or falls back to the default animation (g_ANIM_DEFAULT)
  • it only runs when the doanimate flag is set to true
  • then it increases the frame based on a timer
  • on certain animations, the frame loops back to 1
  • on other animations, the frame will stop at the end of the animation
  • we can also trigger events on specific frame number (eg. play sound)

Next?

Systems are fairly straight forward and fairly short for what they achieve.

We have covered the first set of systems, the next System is the heart of the game: the collision System.


Prev.: Tuto tiny-ecs 2d platformer Part 8 more entities
Next: Tuto tiny-ecs 2d platformer Part 10 Collision System


Tutorial - tiny-ecs 2d platformer