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

From GiderosMobile
(wip)
 
(3 intermediate revisions by the same user not shown)
Line 348: Line 348:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the player1 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 the camera and a sound to add some juice to the game
+
* in ''init'' we add the map definition to constraint the actors movement
* there are 2 kind of hurt animations depending on the player1 health (''washurt'' and ''wasbadlyhurt'')
+
* if an actor is hurt we stop its movement
* when the player1 is hit, we add a camera shake and play some sound
+
* if an actor is within the map definition we move it (player with keyboard, enemies with AI)
* we update the '''HUD'''
+
* then there are the attacks: ground attacks and jump attacks
* when the player1 is dead we play a death sequence and restart the current level
+
* finally sometimes an actor doesn't land after a jump attack so we force it to do so
  
== sNmes.lua ==
+
== sHitboxHurtboxCollision.lua ==
"'''sNmes.lua'''" in the '''"_S"''' folder. The code:
+
"'''sHitboxHurtboxCollision.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
 +
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
 
</syntaxhighlight>
 
</syntaxhighlight>
  
This System deals with the enemies being hit or killed:
+
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'')
 
* 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 pass the list of all the actors (''spriteslist'' declared in '''LevelX''')
* ''onAdd'' some explanation below
+
* first we check if the actors are within range on the y axis with '''checkY'''
* when an enemy is hit, we play some sound
+
* 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">
 +
SCollision = Core.class()
  
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.
+
function SCollision:init(xtiny, xbworld) -- tiny function
 +
xtiny.processingSystem(self) -- called once on init and every update
 +
self.bworld = xbworld
 +
end
  
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.
+
function SCollision:filter(ent) -- tiny function
 +
return ent.collbox and ent.body
 +
end
  
== sAI.lua ==
+
function SCollision:onAdd(ent) -- tiny function
"'''sAI.lua'''" in the '''"_S"''' folder. The code:
+
end
<syntaxhighlight lang="lua">
 
</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''.
+
function SCollision:onRemove(ent) -- tiny function
 +
end
  
== sShadow.lua ==
+
local col -- cbump perfs?
Let's quickly add the Shadow System. "'''sShadow.lua'''" in the '''"_S"''' folder. The code:
+
function SCollision:process(ent, dt) -- tiny function
<syntaxhighlight lang="lua">
+
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
 
</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