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

From GiderosMobile
(wip)
(wip)
Line 356: Line 356:
 
* finally sometimes an actor doesn't land after a jump attack so we force it to do so
 
* finally sometimes an actor doesn't land after a jump attack so we force it to do so
  
== XXX.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
  
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.
+
== XXX.lua ==
 
 
The other thing worth noting is an enemy ability. Depending on the attacks an Entity has, they are stored in an ''abilities'' table. In a artificial intelligence System we will add shortly, the code will iterate the ''abilities'' table and pick a random attack an Entity can perform.
 
 
 
== sAI.lua ==
 
 
"'''sAI.lua'''" in the '''"_S"''' folder. The code:
 
"'''sAI.lua'''" in the '''"_S"''' folder. The code:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">

Revision as of 20:05, 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

XXX.lua

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

This System controls all the entities with an artificial intelligence (ai) id. In this System an entity can be in an idle state, a move state or an attack state. Each states are applied relative to the distance between the Entity and the player1.

sShadow.lua

Let's quickly add the Shadow System. "sShadow.lua" in the "_S" folder. The code:

This System adds a shadow below an Entity. If the Entity is in a jump state, we update the shadow only on the x axis.

Next?

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

We have covered the first set of systems, we have a couple more to add.


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


Tutorial - tiny-ecs beatemup