Tuto tiny-ecs beatemup Part 10 Systems 2
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
XXX.lua
"sNmes.lua" in the "_S" folder. The code:
This System deals with the enemies being hit or killed:
- 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 enemy is hit, we play some sound
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" in the "_S" folder. The code:
This System controls all the entities with an artificial intelligence (ai) id. In this System an entity can be in an idle state, a move state or an attack state. Each states are applied relative to the distance between the Entity and the player1.
sShadow.lua
Let's quickly add the Shadow System. "sShadow.lua" in the "_S" folder. The code:
This System adds a shadow below an Entity. If the Entity is in a jump state, we update the shadow only on the x axis.
Next?
Systems are fairly straight forward and fairly short for what they can achieve.
We have covered the first set of systems, we have a couple more to add.
Prev.: Tuto tiny-ecs beatemup Part 9 Systems
Next: Tuto tiny-ecs beatemup Part 11 XXX