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

From GiderosMobile
(wip)
 
 
(4 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
__TOC__
 
__TOC__
  
== The Systems ==
+
== The Systems 2 ==
We have our entities, we have our components, now the systems. What is an ECS '''System'''?
+
We continue adding systems to our game.
 
 
 
  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 that contain at least one method; an update function that takes parameters like so:
 
   
 
   
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 :-)
+
== sAnimation.lua ==
 
+
Time to animate our actors. Please create a file "'''sAnimation.lua'''" in the '''"_S"''' folder and the code:
To put it simple a '''System''' manipulates entities. Let's see our first '''System'''.
 
 
 
== sDrawable.lua ==
 
Please create a file "'''sDrawable.lua'''" in the '''"_S"''' folder and the code:
 
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
SDrawable = Core.class()
+
SAnimation = Core.class()
  
function SDrawable:init(xtiny) -- tiny function
+
function SAnimation:init(xtiny)
xtiny.system(self) -- called only once on init (no update)
+
xtiny.processingSystem(self) -- called once on init and every frames
 +
self.sndstepgrass = Sound.new("audio/sfx/footstep/Grass02.wav")
 +
self.channel = self.sndstepgrass:play(0, false, true)
 
end
 
end
  
function SDrawable:filter(ent) -- tiny function
+
function SAnimation:filter(ent) -- tiny function
return ent.spritelayer and ent.sprite
+
return ent.animation
 
end
 
end
  
function SDrawable:onAdd(ent) -- tiny function
+
function SAnimation:onAdd(ent) -- tiny function
-- print("SDrawable:onAdd(ent)")
 
ent.spritelayer:addChild(ent.sprite)
 
 
end
 
end
  
function SDrawable:onRemove(ent) -- tiny function
+
function SAnimation:onRemove(ent) -- tiny function
-- print("SDrawable:onRemove(ent)")
+
ent.animation = nil -- free some memory?
ent.spritelayer:removeChild(ent.sprite)
 
-- cleaning?
 
ent.sprite = nil
 
ent = nil
 
 
end
 
end
</syntaxhighlight>
 
  
What it does:
+
local checkanim
* runs only once when it is called
+
function SAnimation:process(ent, dt) -- tiny function
* affects only entities which have a spritelayer variable (id) '''and''' a sprite variable (id)
+
-- a little boost?
* when an Entity is added to tiny-ecs '''World''', the '''System''' adds the Entity to its Sprite layer
+
local anim = ent.animation
* when an Entity is removed from tiny-ecs '''World''', the '''System''' removes the Entity from its Sprite layer
 
  
In other words, the '''System''' adds an Entity to a Sprite layer when the Entity is added to tiny-ecs World, and removes it from that Sprite layer when the Entity is destroyed.
+
-- 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[ent.animation.curranim] then anim.curranim else g_ANIM_DEFAULT
 +
-- print(#anim.anims[checkanim])
  
== sPlayer1Control.lua ==
+
if not ent.doanimate then return end
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:
+
anim.animtimer -= dt
<syntaxhighlight lang="lua">
+
if anim.animtimer < 0 then
SPlayer1Control = Core.class()
+
anim.frame += 1
 
+
anim.animtimer = anim.animspeed
function SPlayer1Control:init(xtiny, xplayer1inputlayer) -- tiny function
+
if checkanim == g_ANIM_DEFAULT then
xtiny.system(self) -- called only once on init (no update)
+
if anim.frame > #anim.anims[checkanim] then
self.player1inputlayer = xplayer1inputlayer
+
anim.frame = 1
end
+
end
 
+
elseif checkanim == g_ANIM_LOSE1_R or checkanim == g_ANIM_STANDUP_R then
function SPlayer1Control:filter(ent) -- tiny function
+
if anim.frame >= #anim.anims[checkanim] then
return ent.isplayer1
+
anim.frame = #anim.anims[checkanim]
end
+
end
 
+
elseif checkanim == g_ANIM_PUNCH_ATTACK1_R then
function SPlayer1Control:onAdd(ent) -- tiny function
+
ent.headhitbox = ent.headhitboxattack1
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
+
if #anim.anims[checkanim] == 1 then -- 1 frame animation
if ent.currlives > 0 then
+
anim.frame = 1
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = true end
+
ent.headhitboxattack1.isactive = true
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = true end
+
ent.isactionpunch1 = false
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = true end
+
else -- multi frames animation
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = true end
+
if anim.frame > #anim.anims[checkanim] then
-- ACTIONS:
+
-- anim.frame = 1
-- isactionpunch1, isactionpunch2, isactionjumppunch1,
+
anim.frame = #anim.anims[checkanim]
-- isactionkick1, isactionkick2, isactionjumpkick1,
+
ent.headhitboxattack1.isactive = false
-- isactionjump1
+
ent.isactionpunch1 = false
if e.keyCode == g_keyaction1 then
+
elseif anim.frame > ent.headhitbox.hitendframe then
ent.animation.frame = 0
+
ent.headhitboxattack1.isactive = false
ent.isactionpunch1 = true
+
elseif anim.frame >= ent.headhitbox.hitstartframe then
elseif e.keyCode == g_keyaction2 then
+
ent.headhitboxattack1.isactive = true
ent.animation.frame = 0
+
end
ent.isactionkick1 = true
+
end
 +
elseif checkanim == g_ANIM_PUNCH_ATTACK2_R then
 +
ent.headhitbox = ent.headhitboxattack2
 +
if anim.frame > #anim.anims[checkanim] then
 +
-- anim.frame = 1
 +
anim.frame = #anim.anims[checkanim]
 +
ent.headhitboxattack2.isactive = false
 +
ent.isactionpunch2 = false
 +
elseif anim.frame > ent.headhitbox.hitendframe then
 +
ent.headhitboxattack2.isactive = false
 +
elseif anim.frame >= ent.headhitbox.hitstartframe then
 +
ent.headhitboxattack2.isactive = true
 
end
 
end
if e.keyCode == g_keyaction3 then
+
elseif checkanim == g_ANIM_KICK_ATTACK1_R then
if ent.body.isonfloor then
+
ent.spinehitbox = ent.spinehitboxattack1
ent.animation.frame = 0
+
if #anim.anims[checkanim] == 1 then -- 1 frame animation
ent.positionystart = ent.pos.y
+
anim.frame = 1
ent.body.isonfloor = false
+
ent.spinehitboxattack1.isactive = true
ent.body.isgoingup = true
+
ent.isactionkick1 = false
ent.isactionjump1 = true
+
else -- multi frames animation
 +
if anim.frame > #anim.anims[checkanim] then
 +
-- anim.frame = 1
 +
anim.frame = #anim.anims[checkanim]
 +
ent.spinehitboxattack1.isactive = false
 +
ent.isactionkick1 = false
 +
elseif anim.frame > ent.spinehitbox.hitendframe then
 +
ent.spinehitboxattack1.isactive = false
 +
elseif anim.frame >= ent.spinehitbox.hitstartframe then
 +
ent.spinehitboxattack1.isactive = true
 
end
 
end
 
end
 
end
end
+
elseif checkanim == g_ANIM_KICK_ATTACK2_R then
end)
+
ent.spinehitbox = ent.spinehitboxattack2
self.player1inputlayer:addEventListener(Event.KEY_UP, function(e)
+
if anim.frame > #anim.anims[checkanim] then
if ent.currlives > 0 then
+
-- anim.frame = 1
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = false end
+
anim.frame = #anim.anims[checkanim]
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = false end
+
ent.spinehitboxattack2.isactive = false
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = false end
+
ent.isactionkick2 = false
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = false end
+
elseif anim.frame > ent.spinehitbox.hitendframe then
-- if e.keyCode == g_keyaction1 then ent.isactionpunch1 = false end
+
ent.spinehitboxattack2.isactive = false
-- if e.keyCode == g_keyaction2 then ent.isactionkick1 = false end
+
elseif anim.frame >= ent.spinehitbox.hitstartframe then
-- if e.keyCode == g_keyaction3 then end
+
ent.spinehitboxattack2.isactive = true
end
+
end
end)
+
elseif checkanim == g_ANIM_JUMP1_R then -- only jump, no attacks
end
+
if anim.frame > #anim.anims[checkanim] then
</syntaxhighlight>
+
anim.frame = #anim.anims[checkanim]
 
+
end
What it does:
+
elseif checkanim == g_ANIM_PUNCHJUMP_ATTACK1_R then
* runs only once when it is called
+
ent.headhitbox = ent.headhitboxjattack1
* affects only entities with the ''isplayer1'' id
+
if anim.frame > #anim.anims[checkanim] then
* when the player1 Entity is added to tiny-ecs '''World''', the System registers KEY_DOWN and KEY_UP events
+
anim.frame = #anim.anims[checkanim]
 
+
ent.headhitboxjattack1.isactive = false
The System processes the user keys input and sets various flags to be processed in a future ''sDynamicBodies'' System we will add.
+
-- ent.isactionjumppunch1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
 
+
else
== sPlayer1.lua ==
+
ent.headhitboxjattack1.isactive = true
"'''sPlayer1.lua'''" in the '''"_S"''' folder. The code:
+
end
<syntaxhighlight lang="lua">
+
elseif checkanim == g_ANIM_KICKJUMP_ATTACK1_R then
SPlayer1 = Core.class()
+
ent.spinehitbox = ent.spinehitboxjattack1
 
+
if anim.frame > #anim.anims[checkanim] then
function SPlayer1:init(xtiny, xcamera) -- tiny function
+
anim.frame = #anim.anims[checkanim]
self.tiny = xtiny -- ref so we can remove entities from tiny system
+
ent.spinehitboxjattack1.isactive = false
self.tiny.processingSystem(self) -- called once on init and every update
+
-- ent.isactionjumpkick1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
-- fx
+
else
self.camera = xcamera -- camera shake
+
ent.spinehitboxjattack1.isactive = true
-- sfx
+
end
self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
+
else
self.channel = self.snd:play(0, false, true)
+
-- player1 steps sound fx
end
+
if ent.isplayer1 and
 
+
(anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
function SPlayer1:filter(ent) -- tiny function
+
(anim.frame == 4 or anim.frame == 9) then
return ent.isplayer1
+
self.channel = self.sndstepgrass:play()
end
+
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
 
 
function SPlayer1:onAdd(ent) -- tiny function
 
end
 
 
 
function SPlayer1:onRemove(ent) -- tiny function
 
end
 
 
 
local resetanim = true
 
function SPlayer1:process(ent, dt) -- tiny function
 
-- hurt fx
 
if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) then
 
ent.washurt -= 1
 
ent.animation.curranim = g_ANIM_HURT_R
 
if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
 
if ent.washurt <= 0 then
 
ent.sprite:setColorTransform(1, 1, 1, 1)
 
self.camera:setZoom(1) -- zoom
 
end
 
elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 then
 
ent.hitfx:setVisible(false)
 
ent.wasbadlyhurt -= 1
 
ent.animation.curranim = g_ANIM_LOSE1_R
 
if ent.wasbadlyhurt < ent.recoverbadtimer/2 then
 
if resetanim then
 
resetanim = false
 
ent.animation.frame = 0
 
 
end
 
end
ent.animation.curranim = g_ANIM_STANDUP_R
+
-- loop animations
end
+
if anim.frame > #anim.anims[checkanim] then
if ent.wasbadlyhurt <= 0 then
+
anim.frame = 1
ent.sprite:setColorTransform(1, 1, 1, 1)
 
self.camera:setZoom(1) -- zoom
 
resetanim = true
 
end
 
end
 
if ent.isdirty then -- hit
 
local function map(v, minSrc, maxSrc, minDst, maxDst, clampValue)
 
local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
 
return not clampValue and newV or clamp(newV, minDst><maxDst, minDst<>maxDst)
 
end
 
self.channel = self.snd:play()
 
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
 
ent.hitfx:setVisible(true)
 
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2+(ent.headhurtbox.x*ent.flip), ent.pos.y+ent.headhurtbox.y-ent.headhurtbox.h/2)
 
ent.spritelayer:addChild(ent.hitfx)
 
ent.currhealth -= ent.damage
 
local hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
 
self.tiny.hudhealth:setWidth(hudhealthwidth)
 
if ent.currhealth < ent.totalhealth/3 then self.tiny.hudhealth:setColor(0xff0000)
 
elseif ent.currhealth < ent.totalhealth/2 then self.tiny.hudhealth:setColor(0xff5500)
 
else self.tiny.hudhealth:setColor(0x00ff00)
 
end
 
ent.washurt = ent.recovertimer -- timer for a flash effect
 
ent.sprite:setColorTransform(2, 0, 0, 2) -- the flash effect (a bright red color)
 
ent.isdirty = false
 
self.camera:shake(0.6, 16) -- (duration, distance), you choose
 
self.camera:setZoom(1.2) -- zoom
 
if ent.currhealth <= 0 then
 
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
 
self.camera:shake(0.8, 64) -- (duration, distance), you choose
 
ent.currlives -= 1
 
for i = 1, ent.totallives do self.tiny.hudlives[i]:setVisible(false) end -- dirty but easy XXX
 
for i = 1, ent.currlives do self.tiny.hudlives[i]:setVisible(true) end -- dirty but easy XXX
 
if ent.currlives > 0 then
 
ent.currhealth = ent.totalhealth
 
hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
 
self.tiny.hudhealth:setWidth(hudhealthwidth)
 
self.tiny.hudhealth:setColor(0x00ff00)
 
if ent.currlives == 1 then self.tiny.hudlives[1]:setColor(0xff0000) end
 
 
end
 
end
 
end
 
end
end
+
anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame])
if ent.currlives <= 0 then -- deaded
 
-- stop all movements
 
ent.isleft = false
 
ent.isright = false
 
ent.isup = false
 
ent.isdown = false
 
-- play dead sequence
 
ent.isdirty = false
 
resetanim = false
 
ent.washurt = ent.recovertimer
 
ent.wasbadlyhurt = ent.recoverbadtimer
 
ent.animation.curranim = g_ANIM_LOSE1_R
 
ent.sprite:setColorTransform(255*0.5/255, 255*0.5/255, 255*0.5/255, 1)
 
self.camera:setZoom(1) -- zoom
 
ent.animation.bmp:setY(ent.animation.bmp:getY()-1)
 
if ent.animation.bmp:getY() < -200 then -- you choose
 
switchToScene(LevelX.new())
 
end
 
 
end
 
end
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the player1 being hit or killed:
+
What the System does:
* runs once on init and '''every game loop''' (''process'')
+
* it runs every frame
* in ''init'' we add the camera and a sound to add some juice to the game
+
* it affects only entities with an ''animation'' id
* there are 2 kind of hurt animations depending on the player1 health (''washurt'' and ''wasbadlyhurt'')
+
* it checks which animation to play
* when the player1 is hit, we add a camera shake and play some sound
+
* it decreases the ''animtimer''
* we update the '''HUD'''
+
* and increases the current frame in the animation by 1
* when the player1 is dead we play a death sequence and restart the current level
+
* if an attack animation is playing it activates/deactivates the corresponding actor '''hit boxes'''
 +
* it eventually plays a sound (player1 steps sound fx)
 +
* it finally sets the current frame in the animation as the actor Bitmap
  
== sNmes.lua ==
+
== sDynamicBodies.lua ==
"'''sNmes.lua'''" in the '''"_S"''' folder. The code:
+
"'''sDynamicBodies.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
SNmes = Core.class()
+
SDynamicBodies = Core.class()
  
 
local random = math.random
 
local random = math.random
  
function SNmes:init(xtiny, xbump) -- tiny function
+
function SDynamicBodies:init(xtiny, xmapdef) -- tiny function
self.tiny = xtiny -- class ref so we can remove entities from tiny world
+
self.tiny = xtiny -- to access self.tiny variables
 
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
+
self.mapdef = xmapdef
-- sfx
 
self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
 
self.channel = self.snd:play(0, false, true)
 
 
end
 
end
  
function SNmes:filter(ent) -- tiny function
+
function SDynamicBodies:filter(ent) -- tiny function
return ent.isnme
+
return ent.body -- only actors with body component
 
end
 
end
  
function SNmes:onAdd(ent) -- tiny function
+
function SDynamicBodies:onAdd(ent) -- tiny function
ent.flip = math.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.isdead = false
 
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 SDynamicBodies:onRemove(ent) -- tiny function
self.bworld:remove(ent) -- remove collision box from cbump world here!
 
 
end
 
end
  
local resetanim = true
+
local randomdir = 0 -- for nmes
function SNmes:process(ent, dt) -- tiny function
+
function SDynamicBodies:process(ent, dt) -- tiny function
-- hurt fx
+
-- is dead or hurt?
if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) and not ent.isdead then
+
if (ent.washurt and ent.washurt > 0) or (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) or ent.currlives <= 0 then
ent.washurt -= 1
+
ent.body.vx = 0
ent.animation.curranim = g_ANIM_HURT_R
+
ent.body.vy = 0
if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
+
return
if ent.washurt <= 0 then ent.sprite:setColorTransform(1, 1, 1, 1) end
+
-- TO REVISIT
elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 and not ent.isdead then
+
end
ent.wasbadlyhurt -= 1
+
-- movements
ent.animation.curranim = g_ANIM_LOSE1_R
+
if ent.isleft and not ent.isright and ent.pos.x > self.mapdef.l then -- LEFT
if ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
+
ent.animation.curranim = g_ANIM_WALK_R
ent.hitfx:setVisible(false)
+
ent.body.vx = -ent.body.currspeed*dt
if resetanim then
+
ent.flip = -1
resetanim = false
+
elseif ent.isright and not ent.isleft and ent.pos.x < self.mapdef.r - ent.w*0.5 then -- RIGHT
ent.animation.frame = 0
+
ent.animation.curranim = g_ANIM_WALK_R
 +
ent.body.vx = ent.body.currspeed*dt
 +
ent.flip = 1
 +
else -- IDLE
 +
ent.animation.curranim = g_ANIM_IDLE_R
 +
ent.body.vx = 0
 +
end
 +
if ent.isup and not ent.isdown and ent.body.isonfloor and ent.pos.y > self.mapdef.t then -- UP
 +
ent.animation.curranim = g_ANIM_WALK_R
 +
ent.body.vy = -ent.body.currspeed*0.015*dt -- 0.01, you choose
 +
elseif ent.isdown and not ent.isup and ent.body.isonfloor and ent.pos.y < self.mapdef.b then -- DOWN
 +
ent.animation.curranim = g_ANIM_WALK_R
 +
ent.body.vy = ent.body.currspeed*0.015*dt -- 0.01, you choose
 +
else
 +
if ent.body.isonfloor then
 +
ent.body.vy = 0
 +
end
 +
end
 +
-- actions
 +
if ent.body.isonfloor then -- GROUND
 +
if ent.isactionpunch1 then
 +
ent.animation.curranim = g_ANIM_PUNCH_ATTACK1_R
 +
ent.body.vx = 0 -- *= 0.1*dt, you choose
 +
ent.body.vy = 0 -- *= 0.1*dt, you choose
 +
elseif ent.isactionpunch2 then
 +
ent.animation.curranim = g_ANIM_PUNCH_ATTACK2_R
 +
ent.body.vx = 0 -- *= 0.1*dt, you choose
 +
ent.body.vy = 0 -- *= 0.1*dt, you choose
 +
elseif ent.isactionkick1 then
 +
ent.animation.curranim = g_ANIM_KICK_ATTACK1_R
 +
ent.body.vx = 0 -- *= 0.1*dt, you choose
 +
ent.body.vy = 0 -- *= 0.1*dt, you choose
 +
elseif ent.isactionkick2 then
 +
ent.animation.curranim = g_ANIM_KICK_ATTACK2_R
 +
ent.body.vx = 0 -- *= 0.1*dt, you choose
 +
ent.body.vy = 0 -- *= 0.1*dt, you choose
 +
end
 +
else -- AIR
 +
if ent.isactionpunch1 then
 +
if ent.isplayer1 and ent.currjumps > 0 then
 +
ent.isactionjumppunch1 = true
 +
end
 +
elseif ent.isactionkick1 then
 +
if ent.isplayer1 and ent.currjumps > 0 then
 +
ent.isactionjumpkick1 = true
 
end
 
end
ent.animation.curranim = g_ANIM_STANDUP_R
 
 
end
 
end
if ent.wasbadlyhurt <= 0 then
+
if ent.isactionjump1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then -- JUMP ONLY
ent.sprite:setColorTransform(1, 1, 1, 1)
+
ent.animation.curranim = g_ANIM_JUMP1_R
resetanim = true
+
if ent.isplayer1 then ent.body.vx *= 2 end -- 3, acceleration, you choose
 +
ent.body.currjumpspeed = ent.body.jumpspeed*1.1 -- higher jump
 +
if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
 +
if ent.pos.y < ent.positionystart - ent.h*0.5 then -- higher apex, you choose
 +
-- ent.body.vx = ent.body.currspeed*dt*4*ent.flip -- acceleration? you choose
 +
ent.body.vy += ent.body.currjumpspeed*1.2 -- falling
 +
ent.body.isgoingup = false
 +
end
 +
if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
 +
ent.body.vy = 0
 +
-- ent.pos.y = ent.positionystart
 +
ent.pos = vector(ent.pos.x, ent.positionystart)
 +
ent.body.isonfloor = true
 +
ent.isactionjump1 = false -- sometimes bug! XXX
 +
end
 
end
 
end
end
+
if ent.isactionjumppunch1 then -- JUMP PUNCH
if ent.isdirty then -- hit
+
ent.animation.curranim = g_ANIM_PUNCHJUMP_ATTACK1_R
self.channel = self.snd:play()
+
if ent.isplayer1 then ent.body.vx *= 2 end -- acceleration, you choose
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
+
ent.body.currjumpspeed = ent.body.jumpspeed
ent.hitfx:setVisible(true)
+
if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
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)
+
if ent.pos.y < ent.positionystart - ent.h*0.45 then -- apex, you choose
ent.spritelayer:addChild(ent.hitfx)
+
-- ent.body.vx = ent.body.currspeed*dt*3*ent.flip -- acceleration? you choose
ent.currhealth -= ent.damage
+
ent.body.vy += ent.body.currjumpspeed*1.2 -- falling
ent.washurt = ent.recovertimer -- timer for a flash effect
+
ent.body.isgoingup = false
-- ent.sprite:setColorTransform(0, 0, 2, 3) -- the flash effect (a bright red color)
+
end
ent.isdirty = false
+
if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
if ent.currhealth <= 0 then
+
ent.body.vy = 0
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for actor to stand back up
+
-- ent.pos.y = ent.positionystart
ent.currlives -= 1
+
ent.pos = vector(ent.pos.x, ent.positionystart)
if ent.currlives > 0 then ent.currhealth = ent.totalhealth end
+
if ent.isnme then
 +
randomdir = random(100)
 +
if randomdir < 50 then ent.isleft = false ent.isright = true
 +
else ent.isleft = true ent.isright = false
 +
end
 +
end
 +
if ent.isplayer1 then
 +
ent.currjumps -= 1
 +
if ent.currjumps < 0 then ent.currjumps = 0 end
 +
self.tiny.hudcurrjumps:setText("JUMPS: "..ent.currjumps)
 +
end
 +
ent.body.isonfloor = true
 +
ent.isactionjump1 = false
 +
ent.isactionpunch1 = false -- TEST
 +
ent.isactionjumppunch1 = false -- sometimes bug! XXX
 +
end
 +
elseif ent.isactionjumpkick1 then -- JUMP KICK
 +
ent.animation.curranim = g_ANIM_KICKJUMP_ATTACK1_R
 +
if ent.isplayer1 then ent.body.vx *= 2 end -- acceleration, you choose
 +
ent.body.currjumpspeed = ent.body.jumpspeed
 +
if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
 +
if ent.pos.y < ent.positionystart - ent.h*0.5 then -- apex, you choose
 +
-- ent.body.vx = ent.body.currspeed*dt*3*ent.flip -- acceleration? you choose
 +
ent.body.vy += ent.body.currjumpspeed*1.25 -- falling
 +
ent.body.isgoingup = false
 +
end
 +
if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
 +
ent.body.vy = 0
 +
-- ent.pos.y = ent.positionystart
 +
ent.pos = vector(ent.pos.x, ent.positionystart)
 +
if ent.isnme then
 +
randomdir = random(100)
 +
if randomdir < 50 then ent.isleft = false ent.isright = true
 +
else ent.isleft = true ent.isright = false
 +
end
 +
end
 +
if ent.isplayer1 then
 +
ent.currjumps -= 1
 +
if ent.currjumps < 0 then ent.currjumps = 0 end
 +
self.tiny.hudcurrjumps:setText("JUMPS: "..ent.currjumps)
 +
end
 +
ent.body.isonfloor = true
 +
ent.isactionjump1 = false
 +
ent.isactionkick1 = false -- TEST
 +
ent.isactionjumpkick1 = false -- sometimes bug!
 +
end
 
end
 
end
 
end
 
end
if ent.currlives <= 0 then -- deaded
+
-- catches the sometimes bug!
-- stop all movements
+
if not ent.body.isonfloor and ent.body.vy == 0 then
ent.isleft = false
+
print("bug", dt)
ent.isright = false
+
ent.body.vy += ent.body.currjumpspeed*16 -- makes the actor touch the ground
ent.isup = false
 
ent.isdown = false
 
-- play dead sequence
 
ent.isdirty = false
 
ent.washurt = ent.recovertimer
 
ent.wasbadlyhurt = ent.recoverbadtimer
 
-- blood
 
if not ent.isdead then
 
ent.hitfx:setVisible(true)
 
ent.hitfx:setColorTransform(3, 0, 0, random(1, 3)/10) -- blood stain
 
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2, ent.pos.y)
 
ent.hitfx:setRotation(random(360))
 
ent.hitfx:setScale(random(5, 8)/10)
 
ent.bgfxlayer:addChild(ent.hitfx)
 
ent.isdead = true
 
end
 
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.numberofnmes -= 1
 
end
 
 
end
 
end
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the enemies being hit or killed:
+
This System is reponsible for moving the actors:
 
* 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 the map definition to constraint the actors movement
* ''onAdd'' some explanation below
+
* if an actor is hurt we stop its movement
* when an enemy is hit, we play some sound
+
* if an actor is within the map definition we move it (player with keyboard, enemies with AI)
 
+
* then there are the attacks: ground attacks and jump attacks
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.
+
* finally sometimes an actor doesn't land after a jump attack so we force it to do so
 
 
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 ==
+
== sHitboxHurtboxCollision.lua ==
"'''sAI.lua'''" in the '''"_S"''' folder. The code:
+
"'''sHitboxHurtboxCollision.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
SAI = Core.class()
+
SHitboxHurtboxCollision = Core.class()
 
 
local random = math.random
 
  
function SAI:init(xtiny, xplayer1) -- tiny function
+
function SHitboxHurtboxCollision:init(xtiny) -- tiny function
 
xtiny.processingSystem(self) -- called once on init and every update
 
xtiny.processingSystem(self) -- called once on init and every update
self.player1 = xplayer1
+
self.spriteslist = xtiny.spriteslist
 
end
 
end
  
function SAI:filter(ent) -- tiny function
+
function SHitboxHurtboxCollision:filter(ent) -- tiny function
return ent.ai
+
return ent.headhitbox or ent.spinehitbox or ent.headhurtbox or ent.spinehurtbox
 
end
 
end
  
function SAI:onAdd(ent) -- tiny function
+
function SHitboxHurtboxCollision:onAdd(ent) -- tiny function
 
end
 
end
  
function SAI:onRemove(ent) -- tiny function
+
function SHitboxHurtboxCollision:onRemove(ent) -- tiny function
 
end
 
end
  
local p1rangex = 192 -- 192 -- magik XXX
+
function SHitboxHurtboxCollision:process(ent, dt) -- tiny function
local p1hitrangex = 64 -- magik XXX
+
local function checkY(actor1y, actor2y, prange)
local p1rangey = 96 -- 96 -- magik XXX
+
return (-(actor1y-actor2y)<>(actor1y-actor2y)) < prange
local p1hitrangey = 16 -- magik XXX
+
end
local rndaction = 0 -- random punch/kick action
+
local function checkCollision(box1x, box1y, box1w, box1h, box2x, box2y, box2w, box2h)
local p1rangetoofar = myappwidth -- disable system to save some CPU, magik XXX
+
return not (box1x - box1w/2 > box2x + box2w/2 or -- is box1 on the right side of box2?
function SAI:process(ent, dt) -- tiny function
+
  box1y - box1h/2 > box2y + box2h/2 or -- is box1 under box2?
if ent.isdead then return end
+
  box1x + box1w/2 < box2x - box2w/2 or -- is box1 on the left side of box2?
local function fun()
+
  box1y + box1h/2 < box2y - box2h/2) -- is box1 above box2?
-- some flags
+
end
ent.doanimate = true -- to save some cpu
+
if ent.headhitbox and ent.headhitbox.isactive then -- HEAD
ent.readytohit = false
+
for k, _ in pairs(self.spriteslist) do -- k = entity, v = true
if (ent.pos.x > self.player1.pos.x + p1rangetoofar or ent.pos.x < self.player1.pos.x - p1rangetoofar) then
+
-- filter out unwanted collisions (player1 vs player1, nme vs nme, ... (you choose!)
ent.doanimate = false
+
if (ent.isplayer1 and k.isplayer1) or
return
+
ent.isdestructibleobject or -- destructible objects only receive damage
end
+
(ent.iscollectible or k.iscollectible) or -- don't check for collectibles
if (ent.pos.x > self.player1.pos.x + p1rangex or ent.pos.x < self.player1.pos.x - p1rangex) or
+
(ent.isnme and k.isnme) or
(ent.pos.y > self.player1.pos.y + p1rangey or ent.pos.y < self.player1.pos.y - p1rangey) then -- OUTSIDE ATTACK RANGE
+
(ent.isnme and k.isdestructibleobject) then -- nmes don't destroy objects, you choose
-- idle
+
-- nothing here!
ent.isleft, ent.isright = false, false
+
-- check head collisions according to actors action and use actor y or actor positionystart
ent.isup, ent.isdown = false, false
+
-- this prevents collisions between wrong hitbox and hurtbox (actors too far away on y axis)
ent.body.currspeed = ent.body.speed
+
elseif (ent.isactionjumppunch1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
ent.body.currjumpspeed = ent.body.jumpspeed
+
if checkY(ent.positionystart, k.positionystart, ent.collbox.h+k.collbox.h) and checkCollision(
else -- INSIDE ATTACK RANGE
+
ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
-- x
+
ent.headhitbox.w, ent.headhitbox.h,
if ent.pos.x > random(self.player1.pos.x+p1hitrangex, self.player1.pos.x+p1rangex) then
+
k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
ent.isleft, ent.isright = true, false
+
k.headhurtbox.w, k.headhurtbox.h) and
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
+
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
elseif ent.pos.x < random(self.player1.pos.x-p1rangex, self.player1.pos.x-p1hitrangex) then
+
k.isdirty = true
ent.isleft, ent.isright = false, true
+
k.damage = ent.headhitbox.damage
ent.body.currspeed = ent.body.speed * random(10, 15)*0.1 -- magik XXX
+
if k.animation then k.animation.frame = 0 end
end
+
ent.headhitbox.isactive = false
-- y
+
else
if ent.pos.y > random(self.player1.pos.y, self.player1.pos.y+p1hitrangey) then
+
k.isdirty = false
ent.isup, ent.isdown = true, false
+
ent.headhitbox.isactive = false
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
+
end
ent.readytohit = true
+
elseif (ent.isactionjumppunch1 or ent.isactionjump1) and not (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
elseif ent.pos.y < random(self.player1.pos.y-p1hitrangey, self.player1.pos.y) then
+
if checkY(ent.positionystart, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
ent.isup, ent.isdown = false, true
+
ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
ent.body.currjumpspeed = ent.body.jumpspeed * random(10, 64) -- magik XXX
+
ent.headhitbox.w, ent.headhitbox.h,
ent.readytohit = true
+
k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
end
+
k.headhurtbox.w, k.headhurtbox.h) and
-- nmes always face player1
+
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
if not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
k.isdirty = true
if ent.pos.x > self.player1.pos.x then ent.flip = -1
+
k.damage = ent.headhitbox.damage
else ent.flip = 1
+
if k.animation then k.animation.frame = 0 end
 +
ent.headhitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.headhitbox.isactive = false
 +
end
 +
elseif not (ent.isactionjumppunch1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
 +
if checkY(ent.pos.y, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
 +
ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
 +
ent.headhitbox.w, ent.headhitbox.h,
 +
k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
 +
k.headhurtbox.w, k.headhurtbox.h) and
 +
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
 +
k.isdirty = true
 +
k.damage = ent.headhitbox.damage
 +
if k.animation then k.animation.frame = 0 end
 +
ent.headhitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.headhitbox.isactive = false
 +
end
 +
else
 +
if checkY(ent.pos.y, k.pos.y, ent.collbox.h+k.collbox.h) and checkCollision(
 +
ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
 +
ent.headhitbox.w, ent.headhitbox.h,
 +
k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
 +
k.headhurtbox.w, k.headhurtbox.h) and
 +
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
 +
k.isdirty = true
 +
k.damage = ent.headhitbox.damage
 +
if k.animation then k.animation.frame = 0 end
 +
ent.headhitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.headhitbox.isactive = false
 
end
 
end
 
end
 
end
 
end
 
end
-- ATTACK
+
elseif ent.spinehitbox and ent.spinehitbox.isactive then -- SPINE
if ent.readytohit then
+
for k, _ in pairs(self.spriteslist) do
ent.curractiontimer -= 1
+
-- filter out unwanted collisions (player1 vs player1, nme vs nme, ... (you choose!)
if ent.curractiontimer < 0 then
+
if (ent.isplayer1 and k.isplayer1) or
ent.animation.frame = 0
+
ent.isdestructibleobject or -- destructible objects only receive damage
rndaction = ent.abilities[random(#ent.abilities)] -- pick a random attack
+
(ent.iscollectible or k.iscollectible) or -- don't check for collectibles
if rndaction == 1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then  
+
(ent.isnme and k.isnme) or
ent.isactionpunch1 = true
+
(ent.isnme and k.isdestructibleobject) then -- nmes don't destroy objects, you choose
elseif rndaction == 2 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
-- nothing here!
ent.isactionpunch2 = true
+
-- check spine collisions according to actors action and use actor y or actor positionystart
elseif rndaction == 3 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
-- this prevents collisions between wrong hitbox and hurtbox (actors too far away on y axis)
ent.isactionkick1 = true
+
elseif (ent.isactionjumpkick1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
elseif rndaction == 4 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
if checkY(ent.positionystart, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
ent.isactionkick2 = true
+
ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
elseif rndaction == 5 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
ent.spinehitbox.w, ent.spinehitbox.h,
ent.positionystart = ent.pos.y
+
k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
ent.body.isonfloor = false
+
k.spinehurtbox.w, k.spinehurtbox.h) and
ent.body.isgoingup = true
+
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
ent.isactionjumppunch1 = true
+
k.isdirty = true
ent.body.currspeed *= 1+random(4)*0.1 -- randomize speed, you choose to add it and the params
+
k.damage = ent.spinehitbox.damage
-- jump in the direction of the flip
+
if k.animation then k.animation.frame = 0 end
if ent.flip == 1 then ent.isleft = false ent.isright = true
+
ent.spinehitbox.isactive = false
else ent.isleft = true ent.isright = false
+
else
end
+
k.isdirty = false
elseif rndaction == 6 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then
+
ent.spinehitbox.isactive = false
ent.positionystart = ent.pos.y
+
end
ent.body.isonfloor = false
+
elseif (ent.isactionjumpkick1 or ent.isactionjump1) and not (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
ent.body.isgoingup = true
+
if checkY(ent.positionystart, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
ent.isactionjumpkick1 = true
+
ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
ent.body.currspeed *= 1+random(3)*0.1 -- randomize speed, you choose to add it and the params
+
ent.spinehitbox.w, ent.spinehitbox.h,
-- jump in the direction of the flip
+
k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
if ent.flip == 1 then ent.isleft = false ent.isright = true
+
k.spinehurtbox.w, k.spinehurtbox.h) and
else ent.isleft = true ent.isright = false
+
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
end
+
k.isdirty = true
 +
k.damage = ent.spinehitbox.damage
 +
if k.animation then k.animation.frame = 0 end
 +
ent.spinehitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.spinehitbox.isactive = false
 +
end
 +
elseif not (ent.isactionjumpkick1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
 +
if checkY(ent.pos.y, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
 +
ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
 +
ent.spinehitbox.w, ent.spinehitbox.h,
 +
k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
 +
k.spinehurtbox.w, k.spinehurtbox.h) and
 +
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
 +
k.isdirty = true
 +
k.damage = ent.spinehitbox.damage
 +
if k.animation then k.animation.frame = 0 end
 +
ent.spinehitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.spinehitbox.isactive = false
 +
end
 +
else
 +
if checkY(ent.pos.y, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
 +
ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
 +
ent.spinehitbox.w, ent.spinehitbox.h,
 +
k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
 +
k.spinehurtbox.w, k.spinehurtbox.h) and
 +
k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
 +
k.isdirty = true
 +
k.damage = ent.spinehitbox.damage
 +
if k.animation then k.animation.frame = 0 end
 +
ent.spinehitbox.isactive = false
 +
else
 +
k.isdirty = false
 +
ent.spinehitbox.isactive = false
 
end
 
end
ent.curractiontimer = ent.actiontimer
 
 
end
 
end
 
end
 
end
Core.yield(1)
 
 
end
 
end
Core.asyncCall(fun) -- profiler seems to be faster without asyncCall (because of pairs traversing?)
 
 
end
 
end
 
</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 checks if a hitbox and a hurtbox overlap then deals damages to the hurt actor:
 +
* runs once on init and '''every game loop''' (''process'')
 +
* in ''init'' we pass the list of all the actors (''spriteslist'' declared in '''LevelX''')
 +
* first we check if the actors are within range on the y axis with '''checkY'''
 +
* then we check if a hitbox and a hurtbox collide with '''checkCollision'''
 +
* when a hitbox is active, meaning an actor is in attack mode, we check if it's a head hit or a body hit
 +
* we also separate the ground attacks from the jump attacks
  
== sShadow.lua ==
+
== sCollision.lua ==
Let's quickly add the Shadow System. "'''sShadow.lua'''" in the '''"_S"''' folder. The code:
+
"'''sCollision.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
SShadow = Core.class()
+
SCollision = Core.class()
  
function SShadow:init(xtiny) -- tiny function
+
function SCollision:init(xtiny, xbworld) -- tiny function
 
xtiny.processingSystem(self) -- called once on init and every update
 
xtiny.processingSystem(self) -- called once on init and every update
 +
self.bworld = xbworld
 
end
 
end
  
function SShadow:filter(ent) -- tiny function
+
function SCollision:filter(ent) -- tiny function
return ent.shadow
+
return ent.collbox and ent.body
 
end
 
end
  
function SShadow:onAdd(ent) -- tiny function
+
function SCollision:onAdd(ent) -- tiny function
ent.spritelayer:addChildAt(ent.shadow.sprite, 1) -- add shadow behind ent
 
 
end
 
end
  
function SShadow:onRemove(ent) -- tiny function
+
function SCollision:onRemove(ent) -- tiny function
ent.spritelayer:removeChild(ent.shadow.sprite)
 
 
end
 
end
  
function SShadow:process(ent, dt) -- tiny function
+
local col -- cbump perfs?
 +
function SCollision:process(ent, dt) -- tiny function
 
local function fun()
 
local function fun()
ent.shadow.sprite:setPosition(ent.pos+vector(ent.collbox.w/2, ent.collbox.h/2))
+
-- collision filter
if ent.body and not ent.body.isonfloor then
+
local function collisionfilter(item, other) -- "touch", "cross", "slide", "bounce"
ent.shadow.sprite:setPosition(ent.pos.x+ent.collbox.w/2, ent.positionystart+ent.collbox.h/2)
+
if item.isactionjump1 or other.isactionjump1 or
 +
item.isactionjumppunch1 or other.isactionjumppunch1 or
 +
item.isactionjumpkick1 or other.isactionjumpkick1 or
 +
item.isdead or other.isdead then return nil
 +
elseif item.iscollectible or other.iscollectible then return "cross"
 +
end return "slide"
 +
end
 +
-- cbump
 +
local goalx = ent.pos.x + ent.body.vx * dt
 +
local goaly = ent.pos.y + ent.body.vy
 +
local nextx, nexty, collisions, len = self.bworld:move(ent, goalx, goaly, collisionfilter)
 +
-- collisions
 +
for i = 1, len do
 +
col = collisions[i]
 +
if col.item.iscollectible and col.other.isplayer1 then col.item.isdirty = true
 +
elseif col.item.isplayer1 and col.other.iscollectible then col.other.isdirty = true
 +
end
 
end
 
end
Core.yield(1)
+
-- move & flip
 +
ent.pos = vector(nextx, nexty)
 +
ent.sprite:setPosition(ent.pos + vector(ent.collbox.w/2, -ent.h/2+ent.collbox.h/2))
 +
ent.animation.bmp:setScale(ent.sx * ent.flip, ent.sy)
 +
Core.yield(0.1)
 
end
 
end
 
Core.asyncCall(fun)
 
Core.asyncCall(fun)
Line 525: Line 597:
 
</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 System uses the '''Bump''' plugin to check for collisions between the actors collision boxes:
 +
* runs once on init and '''every game loop''' (''process'')
 +
* in ''init'' we pass the Bump '''world''' the actors live in
 +
* we define Bump collision filter
 +
* we check if player1 collides with a collectible actor and tag the collectible as dirty
 +
* finally we set the actor position and flip its bitmap in the direction it is going
 +
* I experimented with '''asyncCall''' to test if we could gain some frames per second ;-)
  
 
== Next? ==
 
== Next? ==
Systems are fairly straight forward and fairly short for what they can achieve.
+
Next we add the systems for the breakable objects and the collectibles and we are almost done with the game!
 
 
We have covered the first set of systems, we have a couple more to add.
 
  
  
 
Prev.: [[Tuto tiny-ecs beatemup Part 9 Systems]]</br>
 
Prev.: [[Tuto tiny-ecs beatemup Part 9 Systems]]</br>
'''Next: [[Tuto tiny-ecs beatemup Part 11 XXX]]'''
+
'''Next: [[Tuto tiny-ecs beatemup Part 11 Systems 3]]'''
  
  
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
{{GIDEROS IMPORTANT LINKS}}
 
{{GIDEROS IMPORTANT LINKS}}

Latest revision as of 20:33, 23 November 2024

The Systems 2

We continue adding systems to our game.

A System is a wrapper around function callbacks for manipulating Entities. Systems are implemented as tables that contain at least one method; an update function that takes parameters like so:

function system:update(dt)

There are also a few other optional callbacks:
*function system:filter(entity) Returns true if this System should include this Entity, otherwise should return false. If this isn't specified, no Entities are included in the System.
*function system:onAdd(entity) Called when an Entity is added to the System.
*function system:onRemove(entity) Called when an Entity is removed from the System.
*function system:onModify(dt) Called when the System is modified by adding or removing Entities from the System.
*function system:onAddToWorld(world) Called when the System is added to the World, before any entities are added to the system.
*function system:onRemoveFromWorld(world) Called when the System is removed from the world, after all Entities are removed from the System.
*function system:preWrap(dt) Called on each system before update is called on any system.
*function system:postWrap(dt) Called on each system in reverse order after update is called on each system.

Please see Tiny-ecs#System_functions for more information

sAnimation.lua

Time to animate our actors. Please create a file "sAnimation.lua" in the "_S" folder and the code:

SAnimation = Core.class()

function SAnimation:init(xtiny)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.sndstepgrass = Sound.new("audio/sfx/footstep/Grass02.wav")
	self.channel = self.sndstepgrass:play(0, false, true)
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
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[ent.animation.curranim] then anim.curranim else g_ANIM_DEFAULT
--	print(#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_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_PUNCH_ATTACK1_R then
			ent.headhitbox = ent.headhitboxattack1
			if #anim.anims[checkanim] == 1 then -- 1 frame animation
				anim.frame = 1
				ent.headhitboxattack1.isactive = true
				ent.isactionpunch1 = false
			else -- multi frames animation
				if anim.frame > #anim.anims[checkanim] then
--					anim.frame = 1
					anim.frame = #anim.anims[checkanim]
					ent.headhitboxattack1.isactive = false
					ent.isactionpunch1 = false
				elseif anim.frame > ent.headhitbox.hitendframe then
					ent.headhitboxattack1.isactive = false
				elseif anim.frame >= ent.headhitbox.hitstartframe then
					ent.headhitboxattack1.isactive = true
				end
			end
		elseif checkanim == g_ANIM_PUNCH_ATTACK2_R then
			ent.headhitbox = ent.headhitboxattack2
			if anim.frame > #anim.anims[checkanim] then
--				anim.frame = 1
				anim.frame = #anim.anims[checkanim]
				ent.headhitboxattack2.isactive = false
				ent.isactionpunch2 = false
			elseif anim.frame > ent.headhitbox.hitendframe then
				ent.headhitboxattack2.isactive = false
			elseif anim.frame >= ent.headhitbox.hitstartframe then
				ent.headhitboxattack2.isactive = true
			end
		elseif checkanim == g_ANIM_KICK_ATTACK1_R then
			ent.spinehitbox = ent.spinehitboxattack1
			if #anim.anims[checkanim] == 1 then -- 1 frame animation
				anim.frame = 1
				ent.spinehitboxattack1.isactive = true
				ent.isactionkick1 = false
			else -- multi frames animation
				if anim.frame > #anim.anims[checkanim] then
--					anim.frame = 1
					anim.frame = #anim.anims[checkanim]
					ent.spinehitboxattack1.isactive = false
					ent.isactionkick1 = false
				elseif anim.frame > ent.spinehitbox.hitendframe then
					ent.spinehitboxattack1.isactive = false
				elseif anim.frame >= ent.spinehitbox.hitstartframe then
					ent.spinehitboxattack1.isactive = true
				end
			end
		elseif checkanim == g_ANIM_KICK_ATTACK2_R then
			ent.spinehitbox = ent.spinehitboxattack2
			if anim.frame > #anim.anims[checkanim] then
--				anim.frame = 1
				anim.frame = #anim.anims[checkanim]
				ent.spinehitboxattack2.isactive = false
				ent.isactionkick2 = false
			elseif anim.frame > ent.spinehitbox.hitendframe then
				ent.spinehitboxattack2.isactive = false
			elseif anim.frame >= ent.spinehitbox.hitstartframe then
				ent.spinehitboxattack2.isactive = true
			end
		elseif checkanim == g_ANIM_JUMP1_R then -- only jump, no attacks
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
			end
		elseif checkanim == g_ANIM_PUNCHJUMP_ATTACK1_R then
			ent.headhitbox = ent.headhitboxjattack1
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
				ent.headhitboxjattack1.isactive = false
--				ent.isactionjumppunch1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
			else
				ent.headhitboxjattack1.isactive = true
			end
		elseif checkanim == g_ANIM_KICKJUMP_ATTACK1_R then
			ent.spinehitbox = ent.spinehitboxjattack1
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
				ent.spinehitboxjattack1.isactive = false
--				ent.isactionjumpkick1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
			else
				ent.spinehitboxjattack1.isactive = true
			end
		else
			-- player1 steps sound fx
			if ent.isplayer1 and
				(anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
				(anim.frame == 4 or anim.frame == 9) then
				self.channel = self.sndstepgrass:play()
				if self.channel then self.channel:setVolume(g_sfxvolume*0.01) 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

What the System does:

  • it runs every frame
  • it affects only entities with an animation id
  • it checks which animation to play
  • it decreases the animtimer
  • and increases the current frame in the animation by 1
  • if an attack animation is playing it activates/deactivates the corresponding actor hit boxes
  • it eventually plays a sound (player1 steps sound fx)
  • it finally sets the current frame in the animation as the actor Bitmap

sDynamicBodies.lua

"sDynamicBodies.lua" in the "_S" folder. The code:

SDynamicBodies = Core.class()

local random = math.random

function SDynamicBodies:init(xtiny, xmapdef) -- tiny function
	self.tiny = xtiny -- to access self.tiny variables
	self.tiny.processingSystem(self) -- called once on init and every update
	self.mapdef = xmapdef
end

function SDynamicBodies:filter(ent) -- tiny function
	return ent.body -- only actors with body component
end

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

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

local randomdir = 0 -- for nmes
function SDynamicBodies:process(ent, dt) -- tiny function
	-- is dead or hurt?
	if (ent.washurt and ent.washurt > 0) or (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) or ent.currlives <= 0 then
		ent.body.vx = 0
		ent.body.vy = 0
		return
		-- TO REVISIT
	end
	-- movements
	if ent.isleft and not ent.isright and ent.pos.x > self.mapdef.l then -- LEFT
		ent.animation.curranim = g_ANIM_WALK_R
		ent.body.vx = -ent.body.currspeed*dt
		ent.flip = -1
	elseif ent.isright and not ent.isleft and ent.pos.x < self.mapdef.r - ent.w*0.5 then -- RIGHT
		ent.animation.curranim = g_ANIM_WALK_R
		ent.body.vx = ent.body.currspeed*dt
		ent.flip = 1
	else -- IDLE
		ent.animation.curranim = g_ANIM_IDLE_R
		ent.body.vx = 0
	end
	if ent.isup and not ent.isdown and ent.body.isonfloor and ent.pos.y > self.mapdef.t then -- UP
		ent.animation.curranim = g_ANIM_WALK_R
		ent.body.vy = -ent.body.currspeed*0.015*dt -- 0.01, you choose
	elseif ent.isdown and not ent.isup and ent.body.isonfloor and ent.pos.y < self.mapdef.b then -- DOWN
		ent.animation.curranim = g_ANIM_WALK_R
		ent.body.vy = ent.body.currspeed*0.015*dt -- 0.01, you choose
	else
		if ent.body.isonfloor then
			ent.body.vy = 0
		end
	end
	-- actions
	if ent.body.isonfloor then -- GROUND
		if ent.isactionpunch1 then
			ent.animation.curranim = g_ANIM_PUNCH_ATTACK1_R
			ent.body.vx = 0 -- *= 0.1*dt, you choose
			ent.body.vy = 0 -- *= 0.1*dt, you choose
		elseif ent.isactionpunch2 then
			ent.animation.curranim = g_ANIM_PUNCH_ATTACK2_R
			ent.body.vx = 0 -- *= 0.1*dt, you choose
			ent.body.vy = 0 -- *= 0.1*dt, you choose
		elseif ent.isactionkick1 then
			ent.animation.curranim = g_ANIM_KICK_ATTACK1_R
			ent.body.vx = 0 -- *= 0.1*dt, you choose
			ent.body.vy = 0 -- *= 0.1*dt, you choose
		elseif ent.isactionkick2 then
			ent.animation.curranim = g_ANIM_KICK_ATTACK2_R
			ent.body.vx = 0 -- *= 0.1*dt, you choose
			ent.body.vy = 0 -- *= 0.1*dt, you choose
		end
	else -- AIR
		if ent.isactionpunch1 then
			if ent.isplayer1 and ent.currjumps > 0 then
				ent.isactionjumppunch1 = true
			end
		elseif ent.isactionkick1 then
			if ent.isplayer1 and ent.currjumps > 0 then
				ent.isactionjumpkick1 = true
			end
		end
		if ent.isactionjump1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then -- JUMP ONLY
			ent.animation.curranim = g_ANIM_JUMP1_R
			if ent.isplayer1 then ent.body.vx *= 2 end -- 3, acceleration, you choose
			ent.body.currjumpspeed = ent.body.jumpspeed*1.1 -- higher jump
			if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
			if ent.pos.y < ent.positionystart - ent.h*0.5 then -- higher apex, you choose
--				ent.body.vx = ent.body.currspeed*dt*4*ent.flip -- acceleration? you choose
				ent.body.vy += ent.body.currjumpspeed*1.2 -- falling
				ent.body.isgoingup = false
			end
			if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
				ent.body.vy = 0
--				ent.pos.y = ent.positionystart
				ent.pos = vector(ent.pos.x, ent.positionystart)
				ent.body.isonfloor = true
				ent.isactionjump1 = false -- sometimes bug! XXX
			end
		end
		if ent.isactionjumppunch1 then -- JUMP PUNCH
			ent.animation.curranim = g_ANIM_PUNCHJUMP_ATTACK1_R
			if ent.isplayer1 then ent.body.vx *= 2 end -- acceleration, you choose
			ent.body.currjumpspeed = ent.body.jumpspeed
			if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
			if ent.pos.y < ent.positionystart - ent.h*0.45 then -- apex, you choose
--				ent.body.vx = ent.body.currspeed*dt*3*ent.flip -- acceleration? you choose
				ent.body.vy += ent.body.currjumpspeed*1.2 -- falling
				ent.body.isgoingup = false
			end
			if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
				ent.body.vy = 0
--				ent.pos.y = ent.positionystart
				ent.pos = vector(ent.pos.x, ent.positionystart)
				if ent.isnme then
					randomdir = random(100)
					if randomdir < 50 then ent.isleft = false ent.isright = true
					else ent.isleft = true ent.isright = false
					end
				end
				if ent.isplayer1 then
					ent.currjumps -= 1
					if ent.currjumps < 0 then ent.currjumps = 0 end
					self.tiny.hudcurrjumps:setText("JUMPS: "..ent.currjumps)
				end
				ent.body.isonfloor = true
				ent.isactionjump1 = false
				ent.isactionpunch1 = false -- TEST
				ent.isactionjumppunch1 = false -- sometimes bug! XXX
			end
		elseif ent.isactionjumpkick1 then -- JUMP KICK
			ent.animation.curranim = g_ANIM_KICKJUMP_ATTACK1_R
			if ent.isplayer1 then ent.body.vx *= 2 end -- acceleration, you choose
			ent.body.currjumpspeed = ent.body.jumpspeed
			if ent.body.isgoingup then ent.body.vy -= ent.body.currjumpspeed end
			if ent.pos.y < ent.positionystart - ent.h*0.5 then -- apex, you choose
--				ent.body.vx = ent.body.currspeed*dt*3*ent.flip -- acceleration? you choose
				ent.body.vy += ent.body.currjumpspeed*1.25 -- falling
				ent.body.isgoingup = false
			end
			if not ent.body.isgoingup and ent.pos.y >= ent.positionystart then -- grounded
				ent.body.vy = 0
--				ent.pos.y = ent.positionystart
				ent.pos = vector(ent.pos.x, ent.positionystart)
				if ent.isnme then
					randomdir = random(100)
					if randomdir < 50 then ent.isleft = false ent.isright = true
					else ent.isleft = true ent.isright = false
					end
				end
				if ent.isplayer1 then
					ent.currjumps -= 1
					if ent.currjumps < 0 then ent.currjumps = 0 end
					self.tiny.hudcurrjumps:setText("JUMPS: "..ent.currjumps)
				end
				ent.body.isonfloor = true
				ent.isactionjump1 = false
				ent.isactionkick1 = false -- TEST
				ent.isactionjumpkick1 = false -- sometimes bug!
			end
		end
	end
	-- catches the sometimes bug!
	if not ent.body.isonfloor and ent.body.vy == 0 then
		print("bug", dt)
		ent.body.vy += ent.body.currjumpspeed*16 -- makes the actor touch the ground
	end
end

This System is reponsible for moving the actors:

  • runs once on init and every game loop (process)
  • in init we add the map definition to constraint the actors movement
  • if an actor is hurt we stop its movement
  • if an actor is within the map definition we move it (player with keyboard, enemies with AI)
  • then there are the attacks: ground attacks and jump attacks
  • finally sometimes an actor doesn't land after a jump attack so we force it to do so

sHitboxHurtboxCollision.lua

"sHitboxHurtboxCollision.lua" in the "_S" folder. The code:

SHitboxHurtboxCollision = Core.class()

function SHitboxHurtboxCollision:init(xtiny) -- tiny function
	xtiny.processingSystem(self) -- called once on init and every update
	self.spriteslist = xtiny.spriteslist
end

function SHitboxHurtboxCollision:filter(ent) -- tiny function
	return ent.headhitbox or ent.spinehitbox or ent.headhurtbox or ent.spinehurtbox
end

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

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

function SHitboxHurtboxCollision:process(ent, dt) -- tiny function
	local function checkY(actor1y, actor2y, prange)
		return (-(actor1y-actor2y)<>(actor1y-actor2y)) < prange
	end
	local function checkCollision(box1x, box1y, box1w, box1h, box2x, box2y, box2w, box2h)
		return not (box1x - box1w/2 > box2x + box2w/2 or -- is box1 on the right side of box2?
		   box1y - box1h/2 > box2y + box2h/2 or -- is box1 under box2?
		   box1x + box1w/2 < box2x - box2w/2 or -- is box1 on the left side of box2?
		   box1y + box1h/2 < box2y - box2h/2) -- is box1 above box2?
	end
	if ent.headhitbox and ent.headhitbox.isactive then -- HEAD
		for k, _ in pairs(self.spriteslist) do -- k = entity, v = true
			-- filter out unwanted collisions (player1 vs player1, nme vs nme, ... (you choose!)
			if (ent.isplayer1 and k.isplayer1) or
				ent.isdestructibleobject or -- destructible objects only receive damage
				(ent.iscollectible or k.iscollectible) or -- don't check for collectibles
				(ent.isnme and k.isnme) or
				(ent.isnme and k.isdestructibleobject) then -- nmes don't destroy objects, you choose
				-- nothing here!
			-- check head collisions according to actors action and use actor y or actor positionystart
			-- this prevents collisions between wrong hitbox and hurtbox (actors too far away on y axis)
			elseif (ent.isactionjumppunch1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.positionystart, k.positionystart, ent.collbox.h+k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
					ent.headhitbox.w, ent.headhitbox.h,
					k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
					k.headhurtbox.w, k.headhurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.headhitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.headhitbox.isactive = false
				else
					k.isdirty = false
					ent.headhitbox.isactive = false
				end
			elseif (ent.isactionjumppunch1 or ent.isactionjump1) and not (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.positionystart, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
					ent.headhitbox.w, ent.headhitbox.h,
					k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
					k.headhurtbox.w, k.headhurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.headhitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.headhitbox.isactive = false
				else
					k.isdirty = false
					ent.headhitbox.isactive = false
				end
			elseif not (ent.isactionjumppunch1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.pos.y, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
					ent.headhitbox.w, ent.headhitbox.h,
					k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
					k.headhurtbox.w, k.headhurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.headhitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.headhitbox.isactive = false
				else
					k.isdirty = false
					ent.headhitbox.isactive = false
				end
			else
				if checkY(ent.pos.y, k.pos.y, ent.collbox.h+k.collbox.h) and checkCollision(
				ent.pos.x+ent.collbox.w/2+(ent.headhitbox.x*ent.flip), ent.pos.y+ent.headhitbox.y,
				ent.headhitbox.w, ent.headhitbox.h,
				k.pos.x+k.collbox.w/2+(k.headhurtbox.x*k.flip), k.pos.y+k.headhurtbox.y,
				k.headhurtbox.w, k.headhurtbox.h) and
				k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
					k.isdirty = true
					k.damage = ent.headhitbox.damage
					if k.animation then k.animation.frame = 0 end
					ent.headhitbox.isactive = false
				else
					k.isdirty = false
					ent.headhitbox.isactive = false
				end
			end
		end
	elseif ent.spinehitbox and ent.spinehitbox.isactive then -- SPINE
		for k, _ in pairs(self.spriteslist) do
			-- filter out unwanted collisions (player1 vs player1, nme vs nme, ... (you choose!)
			if (ent.isplayer1 and k.isplayer1) or
				ent.isdestructibleobject or -- destructible objects only receive damage
				(ent.iscollectible or k.iscollectible) or -- don't check for collectibles
				(ent.isnme and k.isnme) or
				(ent.isnme and k.isdestructibleobject) then -- nmes don't destroy objects, you choose
				-- nothing here!
			-- check spine collisions according to actors action and use actor y or actor positionystart
			-- this prevents collisions between wrong hitbox and hurtbox (actors too far away on y axis)
			elseif (ent.isactionjumpkick1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.positionystart, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
					ent.spinehitbox.w, ent.spinehitbox.h,
					k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
					k.spinehurtbox.w, k.spinehurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.spinehitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.spinehitbox.isactive = false
				else
					k.isdirty = false
					ent.spinehitbox.isactive = false
				end
			elseif (ent.isactionjumpkick1 or ent.isactionjump1) and not (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.positionystart, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
					ent.spinehitbox.w, ent.spinehitbox.h,
					k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
					k.spinehurtbox.w, k.spinehurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.spinehitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.spinehitbox.isactive = false
				else
					k.isdirty = false
					ent.spinehitbox.isactive = false
				end
			elseif not (ent.isactionjumpkick1 or ent.isactionjump1) and (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then
				if checkY(ent.pos.y, k.positionystart, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
					ent.spinehitbox.w, ent.spinehitbox.h,
					k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
					k.spinehurtbox.w, k.spinehurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.spinehitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.spinehitbox.isactive = false
				else
					k.isdirty = false
					ent.spinehitbox.isactive = false
				end
			else
				if checkY(ent.pos.y, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision(
					ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y,
					ent.spinehitbox.w, ent.spinehitbox.h,
					k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y,
					k.spinehurtbox.w, k.spinehurtbox.h) and
					k.washurt <= 0 and k.wasbadlyhurt <= 0 then -- <= here!
						k.isdirty = true
						k.damage = ent.spinehitbox.damage
						if k.animation then k.animation.frame = 0 end
						ent.spinehitbox.isactive = false
				else
					k.isdirty = false
					ent.spinehitbox.isactive = false
				end
			end
		end
	end
end

This System checks if a hitbox and a hurtbox overlap then deals damages to the hurt actor:

  • runs once on init and every game loop (process)
  • in init we pass the list of all the actors (spriteslist declared in LevelX)
  • first we check if the actors are within range on the y axis with checkY
  • then we check if a hitbox and a hurtbox collide with checkCollision
  • when a hitbox is active, meaning an actor is in attack mode, we check if it's a head hit or a body hit
  • we also separate the ground attacks from the jump attacks

sCollision.lua

"sCollision.lua" in the "_S" folder. The code:

SCollision = Core.class()

function SCollision:init(xtiny, xbworld) -- tiny function
	xtiny.processingSystem(self) -- called once on init and every update
	self.bworld = xbworld
end

function SCollision:filter(ent) -- tiny function
	return ent.collbox and ent.body
end

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

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

local col -- cbump perfs?
function SCollision:process(ent, dt) -- tiny function
	local function fun()
		-- collision filter
		local function collisionfilter(item, other) -- "touch", "cross", "slide", "bounce"
			if item.isactionjump1 or other.isactionjump1 or
				item.isactionjumppunch1 or other.isactionjumppunch1 or
				item.isactionjumpkick1 or other.isactionjumpkick1 or
				item.isdead or other.isdead then return nil
			elseif item.iscollectible or other.iscollectible then return "cross"
			end return "slide"
		end
		-- cbump
		local goalx = ent.pos.x + ent.body.vx * dt
		local goaly = ent.pos.y + ent.body.vy
		local nextx, nexty, collisions, len = self.bworld:move(ent, goalx, goaly, collisionfilter)
		-- collisions
		for i = 1, len do
			col = collisions[i]
			if col.item.iscollectible and col.other.isplayer1 then col.item.isdirty = true
			elseif col.item.isplayer1 and col.other.iscollectible then col.other.isdirty = true
			end
		end
		-- move & flip
		ent.pos = vector(nextx, nexty)
		ent.sprite:setPosition(ent.pos + vector(ent.collbox.w/2, -ent.h/2+ent.collbox.h/2))
		ent.animation.bmp:setScale(ent.sx * ent.flip, ent.sy)
		Core.yield(0.1)
	end
	Core.asyncCall(fun)
end

This System uses the Bump plugin to check for collisions between the actors collision boxes:

  • runs once on init and every game loop (process)
  • in init we pass the Bump world the actors live in
  • we define Bump collision filter
  • we check if player1 collides with a collectible actor and tag the collectible as dirty
  • finally we set the actor position and flip its bitmap in the direction it is going
  • I experimented with asyncCall to test if we could gain some frames per second ;-)

Next?

Next we add the systems for the breakable objects and the collectibles and we are almost done with the game!


Prev.: Tuto tiny-ecs beatemup Part 9 Systems
Next: Tuto tiny-ecs beatemup Part 11 Systems 3


Tutorial - tiny-ecs beatemup