Difference between revisions of "Tuto tiny-ecs beatemup Part 10 Systems 2"
(wip) |
|||
(4 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
__TOC__ | __TOC__ | ||
− | == The Systems == | + | == The Systems 2 == |
− | We | + | 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''' | ||
− | + | == sAnimation.lua == | |
− | + | Time to animate our actors. Please create a file "'''sAnimation.lua'''" in the '''"_S"''' folder and the code: | |
− | |||
− | |||
− | == | ||
− | Please create a file "''' | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | + | SAnimation = Core.class() | |
− | function | + | function SAnimation:init(xtiny) |
− | 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 | end | ||
− | function | + | function SAnimation:filter(ent) -- tiny function |
− | return ent. | + | return ent.animation |
end | end | ||
− | function | + | function SAnimation:onAdd(ent) -- tiny function |
− | |||
− | |||
end | end | ||
− | function | + | function SAnimation:onRemove(ent) -- tiny function |
− | + | ent.animation = nil -- free some memory? | |
− | ent. | ||
− | |||
− | |||
− | |||
end | 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 | + | 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 | + | 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 | |
− | if | + | 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. | + | ent.headhitboxattack1.isactive = false |
− | ent. | + | elseif anim.frame >= ent.headhitbox.hitstartframe then |
− | elseif | + | ent.headhitboxattack1.isactive = true |
− | ent. | + | end |
− | ent. | + | 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 | + | elseif checkanim == g_ANIM_KICK_ATTACK1_R then |
− | if | + | ent.spinehitbox = ent.spinehitboxattack1 |
− | ent. | + | if #anim.anims[checkanim] == 1 then -- 1 frame animation |
− | ent. | + | anim.frame = 1 |
− | ent. | + | ent.spinehitboxattack1.isactive = true |
− | + | ent.isactionkick1 = false | |
− | ent. | + | 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 | ||
− | + | 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 | |
− | end | + | 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 | end | ||
− | + | -- loop animations | |
− | + | if anim.frame > #anim.anims[checkanim] then | |
− | + | anim.frame = 1 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
− | + | anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame]) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | What the System does: | |
− | * runs | + | * 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: |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | + | SDynamicBodies = Core.class() | |
local random = math.random | local random = math.random | ||
− | function | + | function SDynamicBodies:init(xtiny, xmapdef) -- tiny function |
− | self.tiny = xtiny -- | + | 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. | + | self.mapdef = xmapdef |
− | |||
− | |||
− | |||
end | end | ||
− | function | + | function SDynamicBodies:filter(ent) -- tiny function |
− | return ent. | + | return ent.body -- only actors with body component |
end | end | ||
− | function | + | function SDynamicBodies:onAdd(ent) -- tiny function |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | function | + | function SDynamicBodies:onRemove(ent) -- tiny function |
− | |||
end | end | ||
− | local | + | local randomdir = 0 -- for nmes |
− | function | + | function SDynamicBodies:process(ent, dt) -- tiny function |
− | -- hurt | + | -- is dead or hurt? |
− | if ent.washurt and ent.washurt > 0 | + | if (ent.washurt and ent.washurt > 0) or (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) or ent.currlives <= 0 then |
− | ent. | + | ent.body.vx = 0 |
− | ent.animation.curranim = | + | ent.body.vy = 0 |
− | if ent. | + | return |
− | if ent. | + | -- TO REVISIT |
− | + | end | |
− | + | -- movements | |
− | ent.animation.curranim = | + | 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 | |
− | if | + | ent.flip = -1 |
− | + | elseif ent.isright and not ent.isleft and ent.pos.x < self.mapdef.r - ent.w*0.5 then -- RIGHT | |
− | ent. | + | 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 | ||
− | |||
end | end | ||
− | if ent. | + | if ent.isactionjump1 and not (ent.isactionjumppunch1 or ent.isactionjumpkick1) then -- JUMP ONLY |
− | ent. | + | 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 | 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 | ||
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 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | ent. | ||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | This System | + | 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 | + | * 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: |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | + | SHitboxHurtboxCollision = Core.class() | |
− | |||
− | |||
− | 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. | + | self.spriteslist = xtiny.spriteslist |
end | end | ||
− | function | + | function SHitboxHurtboxCollision:filter(ent) -- tiny function |
− | return ent. | + | return ent.headhitbox or ent.spinehitbox or ent.headhurtbox or ent.spinehurtbox |
end | end | ||
− | function | + | function SHitboxHurtboxCollision:onAdd(ent) -- tiny function |
end | end | ||
− | function | + | function SHitboxHurtboxCollision:onRemove(ent) -- tiny function |
end | end | ||
− | local | + | function SHitboxHurtboxCollision:process(ent, dt) -- tiny function |
− | local | + | 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) | |
− | ent. | + | 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. | + | ent.headhitbox.isactive = false |
− | ent. | + | 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 | |
− | else ent. | + | 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 | ||
− | + | 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. | + | if (ent.isplayer1 and k.isplayer1) or |
− | ent. | + | 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. | + | (ent.isnme and k.isdestructibleobject) then -- nmes don't destroy objects, you choose |
− | + | -- nothing here! | |
− | ent. | + | -- 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. | + | ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y, |
− | elseif | + | ent.spinehitbox.w, ent.spinehitbox.h, |
− | + | k.pos.x+k.collbox.w/2+(k.spinehurtbox.x*k.flip), k.pos.y+k.spinehurtbox.y, | |
− | ent. | + | k.spinehurtbox.w, k.spinehurtbox.h) and |
− | ent. | + | 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 | |
− | ent. | + | end |
− | ent. | + | elseif (ent.isactionjumpkick1 or ent.isactionjump1) and not (k.isactionjumppunch1 or k.isactionjumpkick1 or k.isactionjump1) then |
− | ent. | + | if checkY(ent.positionystart, k.pos.y, ent.collbox.h + k.collbox.h) and checkCollision( |
− | ent. | + | ent.pos.x+ent.collbox.w/2+(ent.spinehitbox.x*ent.flip), ent.pos.y+ent.spinehitbox.y, |
− | ent. | + | 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 | end | ||
− | |||
end | end | ||
− | |||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | This System | + | 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: | |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
− | + | SCollision = Core.class() | |
− | 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 | + | function SCollision:filter(ent) -- tiny function |
− | return ent. | + | return ent.collbox and ent.body |
end | end | ||
− | function | + | function SCollision:onAdd(ent) -- tiny function |
− | |||
end | end | ||
− | function | + | function SCollision:onRemove(ent) -- tiny function |
− | |||
end | end | ||
− | function | + | local col -- cbump perfs? |
+ | function SCollision:process(ent, dt) -- tiny function | ||
local function fun() | 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 | 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 | + | 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? == | ||
− | + | 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]]</br> | Prev.: [[Tuto tiny-ecs beatemup Part 9 Systems]]</br> | ||
− | '''Next: [[Tuto tiny-ecs beatemup Part 11 | + | '''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