Tuto tiny-ecs beatemup Part 10 Systems 2

From GiderosMobile
Revision as of 19:51, 23 November 2024 by MoKaLux (talk | contribs) (wip)

The Systems 2

We continue adding systems to our game.

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

function system:update(dt)

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

Please see Tiny-ecs#System_functions for more information

sAnimation.lua

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

SAnimation = Core.class()

function SAnimation:init(xtiny)
	xtiny.processingSystem(self) -- called once on init and every frames
	self.sndstepgrass = Sound.new("audio/sfx/footstep/Grass02.wav")
	self.channel = self.sndstepgrass:play(0, false, true)
end

function SAnimation:filter(ent) -- tiny function
	return ent.animation
end

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

function SAnimation:onRemove(ent) -- tiny function
	ent.animation = nil -- free some memory?
end

local checkanim
function SAnimation:process(ent, dt) -- tiny function
	-- a little boost?
	local anim = ent.animation

--	checkanim = anim.curranim -- if you are sure all animations are set else use below ternary operator code
	-- luau ternary operator (no end at the end), it's a 1 liner and seems fast?
	checkanim = if anim.anims[ent.animation.curranim] then anim.curranim else g_ANIM_DEFAULT
--	print(#anim.anims[checkanim])

	if not ent.doanimate then return end

	anim.animtimer -= dt
	if anim.animtimer < 0 then
		anim.frame += 1
		anim.animtimer = anim.animspeed
		if checkanim == g_ANIM_DEFAULT then
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = 1
			end
		elseif checkanim == g_ANIM_LOSE1_R or checkanim == g_ANIM_STANDUP_R then
			if anim.frame >= #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
			end
		elseif checkanim == g_ANIM_PUNCH_ATTACK1_R then
			ent.headhitbox = ent.headhitboxattack1
			if #anim.anims[checkanim] == 1 then -- 1 frame animation
				anim.frame = 1
				ent.headhitboxattack1.isactive = true
				ent.isactionpunch1 = false
			else -- multi frames animation
				if anim.frame > #anim.anims[checkanim] then
--					anim.frame = 1
					anim.frame = #anim.anims[checkanim]
					ent.headhitboxattack1.isactive = false
					ent.isactionpunch1 = false
				elseif anim.frame > ent.headhitbox.hitendframe then
					ent.headhitboxattack1.isactive = false
				elseif anim.frame >= ent.headhitbox.hitstartframe then
					ent.headhitboxattack1.isactive = true
				end
			end
		elseif checkanim == g_ANIM_PUNCH_ATTACK2_R then
			ent.headhitbox = ent.headhitboxattack2
			if anim.frame > #anim.anims[checkanim] then
--				anim.frame = 1
				anim.frame = #anim.anims[checkanim]
				ent.headhitboxattack2.isactive = false
				ent.isactionpunch2 = false
			elseif anim.frame > ent.headhitbox.hitendframe then
				ent.headhitboxattack2.isactive = false
			elseif anim.frame >= ent.headhitbox.hitstartframe then
				ent.headhitboxattack2.isactive = true
			end
		elseif checkanim == g_ANIM_KICK_ATTACK1_R then
			ent.spinehitbox = ent.spinehitboxattack1
			if #anim.anims[checkanim] == 1 then -- 1 frame animation
				anim.frame = 1
				ent.spinehitboxattack1.isactive = true
				ent.isactionkick1 = false
			else -- multi frames animation
				if anim.frame > #anim.anims[checkanim] then
--					anim.frame = 1
					anim.frame = #anim.anims[checkanim]
					ent.spinehitboxattack1.isactive = false
					ent.isactionkick1 = false
				elseif anim.frame > ent.spinehitbox.hitendframe then
					ent.spinehitboxattack1.isactive = false
				elseif anim.frame >= ent.spinehitbox.hitstartframe then
					ent.spinehitboxattack1.isactive = true
				end
			end
		elseif checkanim == g_ANIM_KICK_ATTACK2_R then
			ent.spinehitbox = ent.spinehitboxattack2
			if anim.frame > #anim.anims[checkanim] then
--				anim.frame = 1
				anim.frame = #anim.anims[checkanim]
				ent.spinehitboxattack2.isactive = false
				ent.isactionkick2 = false
			elseif anim.frame > ent.spinehitbox.hitendframe then
				ent.spinehitboxattack2.isactive = false
			elseif anim.frame >= ent.spinehitbox.hitstartframe then
				ent.spinehitboxattack2.isactive = true
			end
		elseif checkanim == g_ANIM_JUMP1_R then -- only jump, no attacks
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
			end
		elseif checkanim == g_ANIM_PUNCHJUMP_ATTACK1_R then
			ent.headhitbox = ent.headhitboxjattack1
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
				ent.headhitboxjattack1.isactive = false
--				ent.isactionjumppunch1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
			else
				ent.headhitboxjattack1.isactive = true
			end
		elseif checkanim == g_ANIM_KICKJUMP_ATTACK1_R then
			ent.spinehitbox = ent.spinehitboxjattack1
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = #anim.anims[checkanim]
				ent.spinehitboxjattack1.isactive = false
--				ent.isactionjumpkick1 = false -- don't set to false here otherwise BUGGGZZZZ!!!
			else
				ent.spinehitboxjattack1.isactive = true
			end
		else
			-- player1 steps sound fx
			if ent.isplayer1 and
				(anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
				(anim.frame == 4 or anim.frame == 9) then
				self.channel = self.sndstepgrass:play()
				if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
			end
			-- loop animations
			if anim.frame > #anim.anims[checkanim] then
				anim.frame = 1
			end
		end
		anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame])
	end
end

What the System does:

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

sDynamicBodies.lua

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

SDynamicBodies = Core.class()

local random = math.random

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

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

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

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

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

This System is reponsible for moving the actors:

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

XXX.lua

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

This System deals with the enemies being hit or killed:

  • runs once on init and every game loop (process)
  • in init we add a sound to add some juice to the game
  • onAdd some explanation below
  • when an enemy is hit, we play some sound

In the onAdd function it is worth noting that instead of creating all the variables for the enemies in their Entity code, I found it 'clever' to put them in the enemy System as they all share the same variables.

The other thing worth noting is an enemy ability. Depending on the attacks an Entity has, they are stored in an abilities table. In a artificial intelligence System we will add shortly, the code will iterate the abilities table and pick a random attack an Entity can perform.

sAI.lua

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

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

sShadow.lua

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

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

Next?

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

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


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


Tutorial - tiny-ecs beatemup