Difference between revisions of "Tuto tiny-ecs 2d platformer Part 9 Systems"
(wip) |
|||
| Line 552: | Line 552: | ||
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. | 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. | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
== sAI.lua == | == sAI.lua == | ||
| Line 582: | Line 570: | ||
function SAI:onAdd(ent) -- tiny function | function SAI:onAdd(ent) -- tiny function | ||
| + | -- abilities | ||
| + | -- ent.abilities[#ent.abilities+1] = 1 -- jump/climb | ||
| + | -- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot | ||
| + | -- ent.abilities[#ent.abilities+1] = 20 -- action2, shield | ||
| + | -- ent.abilities[#ent.abilities+1] = 30 -- action3, dash | ||
| + | ent.abilities = {} | ||
| + | if ent.body.upspeed > 0 then | ||
| + | ent.abilities[#ent.abilities+1] = 1 -- jump/climb up | ||
| + | end | ||
| + | if ent.eid == 200 then | ||
| + | ent.abilities[#ent.abilities+1] = 20 -- action2, shield | ||
| + | if ent.body.speed > 0 then | ||
| + | ent.abilities[#ent.abilities+1] = 30 -- action3, dash | ||
| + | end | ||
| + | elseif ent.eid == 300 then -- nme boss1 | ||
| + | ent.abilities[#ent.abilities+1] = 10 -- action1, shoot | ||
| + | ent.abilities[#ent.abilities+1] = 20 -- action2, shield | ||
| + | if ent.body.speed > 0 then | ||
| + | ent.abilities[#ent.abilities+1] = 30 -- action3, dash | ||
| + | end | ||
| + | else | ||
| + | ent.abilities[#ent.abilities+1] = 20 -- action2, shield | ||
| + | end | ||
| + | -- for k, v in pairs(ent.abilities) do print(k, v) end | ||
end | end | ||
| Line 587: | Line 599: | ||
end | end | ||
| − | local | + | local p1rangetoofarx = myappwidth*2 -- 0.7, disable systems to save some CPU, magik XXX |
| − | local | + | local p1rangetoofary = myappheight*2 -- 0.7, disable systems to save some CPU, magik XXX |
| − | local | + | local p1outofrangex = myappwidth*1 -- 0.5, magik XXX |
| − | local | + | local p1outofrangey = myappheight*1 -- 0.5, magik XXX |
| − | local | + | local p1actionrange = myappwidth*0.2 -- 3*8, 1*8, myappwidth*0.1, magik XXX |
| − | |||
function SAI:process(ent, dt) -- tiny function | function SAI:process(ent, dt) -- tiny function | ||
| − | if ent. | + | if ent.currlives <= 0 then |
| + | return | ||
| + | end | ||
local function fun() | local function fun() | ||
-- some flags | -- some flags | ||
ent.doanimate = true -- to save some cpu | ent.doanimate = true -- to save some cpu | ||
| − | + | -- OUTSIDE VISIBLE RANGE | |
| − | if (ent.pos.x > self.player1.pos.x + | + | if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or |
| + | ent.pos.x < self.player1.pos.x - p1rangetoofarx) or | ||
| + | (ent.pos.y > self.player1.pos.y + p1rangetoofary or | ||
| + | ent.pos.y < self.player1.pos.y - p1rangetoofary) then | ||
ent.doanimate = false | ent.doanimate = false | ||
return | return | ||
end | end | ||
| − | if (ent.pos.x > self.player1.pos.x + | + | -- OUTSIDE ACTION RANGE |
| − | (ent.pos.y > self.player1.pos.y + | + | if (ent.pos.x > self.player1.pos.x+p1outofrangex or |
| + | ent.pos.x < self.player1.pos.x-p1outofrangex) or | ||
| + | (ent.pos.y > self.player1.pos.y+p1outofrangey or | ||
| + | ent.pos.y < self.player1.pos.y-p1outofrangey) then | ||
-- idle | -- idle | ||
ent.isleft, ent.isright = false, false | ent.isleft, ent.isright = false, false | ||
ent.isup, ent.isdown = false, false | ent.isup, ent.isdown = false, false | ||
ent.body.currspeed = ent.body.speed | ent.body.currspeed = ent.body.speed | ||
| − | ent.body. | + | ent.body.currupspeed = ent.body.upspeed |
| − | else -- INSIDE | + | ent.readyforaction = false |
| + | else -- INSIDE ACTION RANGE | ||
-- x | -- x | ||
| − | if ent.pos.x > random(self.player1.pos.x+ | + | local rnd = random(100) |
| − | + | if rnd > 1 and ent.body.speed > 0 then -- allow nme to stop walking, magik XXX | |
| − | + | if ent.pos.x > random(self.player1.pos.x+p1actionrange, self.player1.pos.x+p1outofrangex) then | |
| − | + | ent.isleft, ent.isright = true, false | |
| − | + | ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX | |
| − | + | elseif ent.pos.x < random(self.player1.pos.x-p1outofrangex, self.player1.pos.x-p1actionrange) then | |
| + | ent.isleft, ent.isright = false, true | ||
| + | ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX | ||
| + | end | ||
| + | elseif ent.body.speed == 0 then -- nme always faces player1 | ||
| + | if ent.pos.x > self.player1.pos.x then | ||
| + | ent.isleft, ent.isright = true, false | ||
| + | else | ||
| + | ent.isleft, ent.isright = false, true | ||
| + | end | ||
| + | else -- stop moving | ||
| + | ent.isleft, ent.isright = false, false | ||
| + | ent.body.currspeed = ent.body.speed | ||
| + | end | ||
| + | -- no going left or right on stairs/ladders | ||
| + | if ent.isladdercontacts and not (ent.isfloorcontacts or ent.isptpfcontacts) then | ||
| + | ent.isright, ent.isleft = false, false | ||
| + | end | ||
| + | -- bumping on a wall unless isleftofplayer | ||
| + | ent.isleftofplayer = false | ||
| + | if ent.pos.x < self.player1.pos.x then | ||
| + | ent.isleftofplayer = true | ||
end | end | ||
-- y | -- y | ||
| − | if ent. | + | -- impulse on top of stairs/ladders and actor is going up |
| + | if ent.isladdercontacts and ent.isptpfcontacts and ent.body.vy < 0 then | ||
ent.isup, ent.isdown = true, false | ent.isup, ent.isdown = true, false | ||
| − | ent. | + | ent.wasup = false |
| − | + | ent.body.currinputbuffer = ent.body.inputbuffer | |
| − | + | ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX | |
| − | |||
| − | ent.body. | ||
| − | |||
end | end | ||
| − | -- | + | -- only act if entity has abilities |
| − | if | + | if #ent.abilities > 0 then |
| − | + | ent.readyforaction = true | |
| − | |||
| − | |||
end | end | ||
end | end | ||
| − | -- | + | -- ACTION |
| − | if ent. | + | if ent.readyforaction then |
ent.curractiontimer -= 1 | ent.curractiontimer -= 1 | ||
| + | -- don't let the entity get stuck in place when in range | ||
| + | if ent.body.vx == 0 and ent.body.speed > 0 then | ||
| + | if ent.pos.x > self.player1.pos.x then ent.isleft, ent.isright = true, false | ||
| + | else ent.isleft, ent.isright = false, true | ||
| + | end | ||
| + | ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX | ||
| + | end | ||
if ent.curractiontimer < 0 then | if ent.curractiontimer < 0 then | ||
| − | + | -- actor above player1, go down | |
| − | + | if ent.pos.y < self.player1.pos.y and ent.body.upspeed > 0 then | |
| − | if | + | if ent.isptpfcontacts then |
| − | + | local rnd = random(100) | |
| − | + | if rnd > 30 then -- 80, magik XXX | |
| − | + | ent.isup, ent.isdown = false, true | |
| − | + | ent.wasdown = false | |
| − | ent. | + | ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX |
| − | + | end | |
| − | + | elseif ent.body.upspeed > 0 then -- ladders/stairs | |
| − | + | ent.isup, ent.isdown = false, true | |
| − | + | ent.wasdown = false | |
| − | + | ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX | |
| − | |||
| − | |||
| − | |||
| − | -- | ||
| − | |||
| − | |||
end | end | ||
| − | + | end | |
| − | + | -- actor below player1, jump | |
| − | + | if ent.pos.y > self.player1.pos.y + 8 and ent.body.upspeed > 0 then | |
| − | + | local rnd = random(100) | |
| − | + | if rnd > 40 then -- 80, magik XXX | |
| − | + | ent.isup, ent.isdown = true, false | |
| − | + | ent.wasup = false | |
| − | + | ent.body.currinputbuffer = ent.body.inputbuffer | |
| − | + | ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX | |
end | end | ||
end | end | ||
| − | ent.curractiontimer = ent.actiontimer | + | -- pick a random available action |
| + | local rndaction = ent.abilities[random(#ent.abilities)] | ||
| + | -- movements | ||
| + | if rndaction == 1 and ent.body.upspeed > 0 then -- jump | ||
| + | local rnd = random(100) | ||
| + | if rnd > 70 then -- 80, jump | ||
| + | ent.isup, ent.isdown = true, false | ||
| + | ent.wasup = false | ||
| + | ent.body.currinputbuffer = ent.body.inputbuffer | ||
| + | ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX | ||
| + | end | ||
| + | end | ||
| + | -- actions | ||
| + | local rnd = random(100) | ||
| + | if rndaction == 10 then -- shoot | ||
| + | ent.isaction1 = false | ||
| + | if rnd > 99 then -- rate | ||
| + | ent.isaction1 = true | ||
| + | end | ||
| + | elseif rndaction == 20 then -- shield | ||
| + | ent.isaction2 = false | ||
| + | if rnd > 98 then -- rate | ||
| + | ent.isaction2 = true | ||
| + | end | ||
| + | elseif rndaction == 30 then -- dash | ||
| + | ent.isaction3 = false | ||
| + | if rnd > 95 then -- rate | ||
| + | ent.body.currspeed = ent.body.speed*0.5 -- magik XXX | ||
| + | ent.isaction3 = true | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | -- extra attack on jumping peak | ||
| + | if ent.body.vy > 0 and ent.body.vy < 30 and ent.body.upspeed > 0 then -- magik XXX | ||
| + | if ent.curractiontimer < 0 then | ||
| + | local rndaction = ent.abilities[random(#ent.abilities)] | ||
| + | local rnd = random(100) | ||
| + | if rnd > 70 then -- rate | ||
| + | if rndaction == 10 then -- shoot | ||
| + | ent.isaction1 = true | ||
| + | elseif rndaction == 20 then -- shield | ||
| + | ent.isaction2 = true | ||
| + | end | ||
| + | else -- dash/shield | ||
| + | local rnd2 = random(100) | ||
| + | if rnd2 > 30 then | ||
| + | if rndaction == 30 then -- dash | ||
| + | ent.isaction3 = true | ||
| + | end | ||
| + | else | ||
| + | ent.body.currspeed = ent.body.speed*0.5 -- magik XXX | ||
| + | if rndaction == 20 then -- shield | ||
| + | ent.isaction2 = true | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | ent.curractiontimer = ent.actiontimer | ||
| + | end | ||
end | end | ||
end | end | ||
| Line 680: | Line 775: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | This System controls all the entities with an artificial intelligence (''ai'') id | + | This System controls all the entities with an artificial intelligence (''ai'') id: |
| + | * runs once on init and every game loop (process) | ||
| + | * '''onAdd()''' we assign various abilities depending on the type of enemy id | ||
| + | <syntaxhighlight lang="lua"> | ||
| + | -- abilities | ||
| + | ent.abilities = {} | ||
| + | -- ent.abilities[#ent.abilities+1] = 1 -- jump/climb | ||
| + | -- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot | ||
| + | -- ent.abilities[#ent.abilities+1] = 20 -- action2, shield | ||
| + | -- ent.abilities[#ent.abilities+1] = 30 -- action3, dash | ||
| + | </syntaxhighlight> | ||
| + | * '''process()''' when the enemy is outside the visible screen we don't process it | ||
| + | * '''process()''' when the enemy is in action range with the player1 we randomly trigger an action (ability) | ||
| − | == | + | == sAnimation.lua == |
| − | + | The animation System. "'''sAnimation.lua'''" in the '''"_S"''' folder. The code: | |
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
| − | + | SAnimation = Core.class() | |
| − | function | + | function SAnimation:init(xtiny) |
| − | xtiny.processingSystem(self) -- called once on init and every | + | xtiny.processingSystem(self) -- called once on init and every frames |
| + | self.sndstepgrass = { sound=Sound.new("audio/sfx/footstep/Grass02.wav"), time=0, delay=0.2, } | ||
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. | + | ent.animation = nil -- free some memory? |
end | end | ||
| − | function | + | local checkanim |
| − | local | + | --@native -- new in Gideros |
| − | + | function SAnimation:process(ent, dt) -- tiny function | |
| − | if | + | -- a little boost? |
| − | ent. | + | 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[anim.curranim] then anim.curranim else g_ANIM_DEFAULT | ||
| + | -- print("checkanim", checkanim) | ||
| + | -- print("#anim.anims[checkanim]", #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_HURT_R or 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_JUMPUP_R or checkanim == g_ANIM_JUMPDOWN_R then | ||
| + | if anim.frame > #anim.anims[checkanim] then | ||
| + | anim.frame = #anim.anims[checkanim] | ||
| + | end | ||
| + | else -- any looping animation | ||
| + | -- player1 steps sound fx | ||
| + | if ent.isplayer1 then | ||
| + | if (anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and | ||
| + | (anim.frame == 3 or anim.frame == 7) then | ||
| + | local snd = self.sndstepgrass | ||
| + | local curr = os.timer() | ||
| + | local prev = snd.time | ||
| + | if curr - prev > snd.delay then | ||
| + | local channel = snd.sound:play() | ||
| + | if channel then channel:setVolume(g_sfxvolume*0.01) end | ||
| + | snd.time = curr | ||
| + | end | ||
| + | end | ||
| + | end | ||
| + | -- loop animations | ||
| + | if anim.frame > #anim.anims[checkanim] then | ||
| + | anim.frame = 1 | ||
| + | end | ||
end | end | ||
| − | + | anim.bmp:setTextureRegion(anim.anims[checkanim][anim.frame]) | |
end | end | ||
| − | |||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | This System | + | This is the System responsible for all the animations in the game: |
| + | * runs once on init and every game loop (process) | ||
| + | * it executes an animation or falls back to the default animation ('''g_ANIM_DEFAULT''') | ||
| + | * it only runs when the ''doanimate'' flag is set to true | ||
| + | * then it increases the frame based on a timer | ||
| + | * on certain animations, the frame loops back to 1 | ||
| + | * on other animations, the frame will stop at the end of the animation | ||
| + | * we can also trigger events on specific frame number (eg. play sound) | ||
== Next? == | == Next? == | ||
| − | Systems are fairly straight forward and fairly short for what they | + | Systems are fairly straight forward and fairly short for what they achieve. |
We have covered the first set of systems, we have a couple more to add. | We have covered the first set of systems, we have a couple more to add. | ||
Revision as of 06:56, 5 November 2025
The Systems
We have our entities, we have our components, now the systems. What is an ECS System?
A System is a wrapper around function callbacks for manipulating Entities. Systems are implemented as tables. There are a few 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:process(entity, dt) Called once on init and every frames. *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
Don't worry, we won't use all the callback functions :-)
sDrawable.lua
Our first System will add/remove sprites from layers. Please create a file "sDrawable.lua" in the "_S" folder and the code:
SDrawable = Core.class()
function SDrawable:init(xtiny) -- tiny function
xtiny.system(self) -- called only once on init (no update)
end
function SDrawable:filter(ent) -- tiny function
return ent.spritelayer and ent.sprite
end
function SDrawable:onAdd(ent) -- tiny function
-- print("SDrawable:onAdd(ent)", ent.pos)
ent.spritelayer:addChild(ent.sprite)
end
function SDrawable:onRemove(ent) -- tiny function
-- print("SDrawable:onRemove(ent)")
-- if ent.isplayer1 then return end
if ent.ispersistent then return end
ent.spritelayer:removeChild(ent.sprite)
-- cleaning?
ent.sprite = nil
ent = nil
end
What it does:
- runs only once when it is called
- affects only entities which have a spritelayer variable (id) and a sprite variable (id)
- when an Entity is added to tiny-ecs World, the System adds the Entity to its Sprite layer
- when an Entity is removed from tiny-ecs World, the System removes the Entity from its Sprite layer unless it has a ispersistent id
In other words, the System adds an Entity to a Sprite layer when the Entity is added to tiny-ecs World, and removes it from that Sprite layer when the Entity is destroyed.
sPlayer1Control.lua
I am adding systems in an order that seems logical and helps in understanding the making of the game. The next System we add is the player1 controller.
"sPlayer1Control.lua" in the "_S" folder. The code:
SPlayer1Control = Core.class()
function SPlayer1Control:init(xtiny, xplayer1inputlayer) -- tiny function
xtiny.system(self) -- called only once on init (no update)
self.player1inputlayer = xplayer1inputlayer
end
function SPlayer1Control:filter(ent) -- tiny function
return ent.isplayer1
end
function SPlayer1Control:onAdd(ent) -- tiny function
-- listeners
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then -- left
ent.isleft = true
elseif e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
ent.isright = true
end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then -- up
ent.isup = true
ent.wasup = false -- allows initial jump
ent.body.currinputbuffer = ent.body.inputbuffer
elseif e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then -- down
ent.isdown = true
end
-- ACTIONS:
if e.keyCode == g_keyaction1 then -- shoot
if ent.shield.currtimer <= 0 then -- no shoot while shield active, you choose!
ent.isaction1 = true
end
elseif e.keyCode == g_keyaction2 then -- shield
ent.isaction2 = true
elseif e.keyCode == g_keyaction3 then -- dash
if ent.body.currdashcooldown <= 0 then
ent.isaction3 = true
end
end
end
end)
self.player1inputlayer:addEventListener(Event.KEY_UP, function(e)
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then -- left
ent.isleft = false
end
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then -- right
ent.isright = false
end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then -- up
ent.isup = false
ent.wasup = false -- prevent constant jumps
if ent.body.vy < ent.body.upspeed*0.5 then -- variable jump height
local function lerp(a,b,t) return a + (b-a) * t end
-- ent.body.vy = lerp(ent.body.vy, ent.body.upspeed*0.5, 0.5)
ent.body.vy = lerp(ent.body.vy, ent.body.upspeed*0.8333, 0.25)
end
end
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then -- down
ent.isdown = false
ent.wasdown = false -- prevent constant going down ptpf
end
if e.keyCode == g_keyaction1 then ent.isaction1 = false end
if e.keyCode == g_keyaction2 then ent.isaction2 = false end
if e.keyCode == g_keyaction3 then ent.isaction3 = false end
end
end)
end
What it does:
- runs only once when it is called
- affects only entities with the isplayer1 id
- when the player1 Entity is added to tiny-ecs World, the System registers KEY_DOWN and KEY_UP events
The System processes the user keys input and sets various flags to be processed in a future collision System we will add.
sPlayer1.lua
"sPlayer1.lua" in the "_S" folder. The code:
SPlayer1 = Core.class()
function SPlayer1:init(xtiny, xbump, xcamera) -- tiny function
self.tiny = xtiny -- ref so we can remove entities from tiny system
self.tiny.processingSystem(self) -- called once on init and every update
self.bworld = xbump
-- fx
self.camera = xcamera -- camera shake
self.camcurrzoom = self.camera:getZoom()
-- sfx
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
end
function SPlayer1:filter(ent) -- tiny function
return ent.isplayer1
end
function SPlayer1:onAdd(ent) -- tiny function
end
function SPlayer1:onRemove(ent) -- tiny function
self.bworld:remove(ent) -- remove collision box from cbump world here!
end
function SPlayer1:process(ent, dt) -- tiny function
if ent.isaction1 then -- shoot
ent.isaction1 = false
local projectilespeed = 60*8 -- 54*8
local xangle = ^<0
if ent.flip == -1 then xangle = ^<180 end
local vx, vy = projectilespeed * math.cos(xangle), projectilespeed * math.sin(xangle)
--EProjectiles:init(xid, xmass, xangle, xspritelayer, xpos, xvx, xvy, dx, dy, xpersist)
local p = EProjectiles.new(
1, 0, xangle, ent.spritelayer,
ent.pos + vector(ent.collbox.w*0.5, ent.collbox.h*0.4),
vx, vy, 36*8, 40*8, false
)
p.body.vx = vx
p.body.vy = vy
self.tiny.tworld:addEntity(p)
self.bworld:add(p, p.pos.x, p.pos.y, p.collbox.w, p.collbox.h)
end
if ent.isaction2 then -- shield
ent.isaction2 = false
ent.shield.currtimer = ent.shield.timer
ent.shield.sprite:setVisible(true)
end
if ent.isaction3 and ent.body.candash then -- dash
ent.isaction3 = false
ent.body.currdashtimer = ent.body.dashtimer
ent.body.currdashcooldown = ent.body.dashcooldown
end
-- shield collision
if ent.shield.currtimer > 0 then
local function collisionfilter2(item) -- only one param: "item", return true, false or nil
if item.isnme or (item.isprojectile and item.eid > 1) then
return true
end
end
ent.shield.sprite:setScale(ent.shield.sprite.sx*ent.flip, ent.shield.sprite.sy)
ent.shield.sprite:setPosition(
ent.pos + vector(ent.collbox.w/2, 0) +
ent.shield.offset*vector(ent.shield.sprite.sx*ent.flip, ent.shield.sprite.sy)
)
local pw, ph = ent.shield.sprite:getWidth(), ent.shield.sprite:getHeight()
--local items, len = world:queryRect(l, t, w, h, filter)
local items, len2 = self.bworld:queryRect(
ent.pos.x+ent.shield.offset.x*ent.flip-pw*0.5+ent.collbox.w*0.5,
ent.pos.y+ent.shield.offset.y-ph*0.5,
pw, ph,
collisionfilter2
)
for i = 1, len2 do
local item = items[i]
if ent.shield.currtimer > 0 then
item.damage = ent.shield.damage
item.isdirty = true
end
end
end
if ent.shield and ent.shield.currtimer > 0 then
ent.shield.currtimer -= 1
if ent.shield.currtimer <= 0 then
ent.shield.sprite:setVisible(false)
end
end
-- hurt
if ent.washurt > 0 and ent.wasbadlyhurt <= 0 and ent.currlives > 0 then -- lose 1 nrg
ent.washurt -= 1
ent.isdirty = false
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
self.camera:setZoom(self.camcurrzoom) -- zoom
elseif ent.washurt < ent.recovertimer*0.5 then
ent.hitfx:setVisible(false)
end
elseif ent.wasbadlyhurt > 0 and ent.currlives > 0 then -- lose 1 life
ent.wasbadlyhurt -= 1
ent.isdirty = false
if ent.wasbadlyhurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1)
self.camera:setZoom(self.camcurrzoom) -- zoom
elseif ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
ent.hitfx:setVisible(false)
ent.animation.curranim = g_ANIM_STANDUP_R
if ent.iswallcontacts then
ent.animation.curranim = g_ANIM_WALL_IDLE_R
end
ent.animation.frame = 0 -- start animation at frame 0
end
end
if ent.body.currdashtimer > 0 then -- invicible while dashing
ent.isdirty = false
end
-- hit
if ent.isdirty then
local snd = self.snd -- sfx
local curr = os.timer()
local prev = snd.time
if curr - prev > snd.delay then
local channel = snd.sound:play()
if channel then channel:setVolume(g_sfxvolume*0.01) end
snd.time = curr
end
local function map(v, minSrc, maxSrc, minDst, maxDst, clampValue)
local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
return not clampValue and newV or clamp(newV, minDst><maxDst, minDst<>maxDst)
end
ent.sprite:setColorTransform(1, 1, 0, 2)
ent.hitfx:setVisible(true)
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
ent.spritelayer:addChild(ent.hitfx)
ent.currhealth -= ent.damage
-- hud
local hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
self.tiny.hudhealth:setWidth(hudhealthwidth)
if ent.currhealth < ent.totalhealth/3 then self.tiny.hudhealth:setColor(0xff0000)
elseif ent.currhealth < ent.totalhealth/2 then self.tiny.hudhealth:setColor(0xff5500)
else self.tiny.hudhealth:setColor(0x00ff00)
end
ent.washurt = ent.recovertimer -- timer for a flash effect
ent.animation.curranim = g_ANIM_HURT_R
ent.animation.frame = 0 -- start animation at frame 0
ent.isdirty = false
if ent.currhealth <= 0 then
ent.sprite:setColorTransform(1, 0, 0, 2)
ent.currlives -= 1
-- hud
for i = 1, ent.totallives do self.tiny.hudlives[i]:setVisible(false) end -- dirty but easy XXX
for i = 1, ent.currlives do self.tiny.hudlives[i]:setVisible(true) end -- dirty but easy XXX
if ent.currlives > 0 then
ent.currhealth = ent.totalhealth
hudhealthwidth = map(ent.currhealth, 0, ent.totalhealth, 0, 100)
self.tiny.hudhealth:setWidth(hudhealthwidth)
self.tiny.hudhealth:setColor(0x00ff00)
if ent.currlives == 1 then self.tiny.hudlives[1]:setColor(0xff0000) end
end
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
ent.animation.curranim = g_ANIM_LOSE1_R
if ent.iswallcontacts then ent.animation.curranim = g_ANIM_HURT_R end
self.camera:shake(0.8, 64) -- (duration, distance), you choose
end
end
-- deaded
if ent.currlives <= 0 then
-- stop all movements
ent.isleft = false
ent.isright = false
ent.isup = false
ent.isdown = false
-- play dead sequence
ent.isdirty = false
ent.washurt = 0
ent.wasbadlyhurt = 0
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(1, 1, 1, 5)
self.camera:setZoom(self.camcurrzoom) -- zoom
ent.body.currmass = -0.15 -- soul leaves body!
ent.isup = true
local timer = Timer.new(2000, 1)
timer:addEventListener(Event.TIMER_COMPLETE, function(e)
ent.restart = true
end)
timer:start()
end
end
This System deals with the player1 actions:
- 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
- when an action button is pressed we execute it (action1 = shoot, action2 = shield, action3 = dash)
- there are 2 kind of hurt animations depending on the player1 health (washurt and wasbadlyhurt)
- when the player1 is hit, we add a camera shake and play some sound
- we update the HUD
- when the player1 is dead we play a death sequence and restart the current level
sNmes.lua
"sNmes.lua" in the "_S" folder. The code:
SNmes = Core.class()
local random, atan2, cos, sin = math.random, math.atan2, math.cos, math.sin
function SNmes:init(xtiny, xbump, xplayer1) -- tiny function
xtiny.processingSystem(self) -- called once on init and every update
self.tiny = xtiny -- class ref so we can remove entities from tiny world
self.bworld = xbump
self.player1 = xplayer1
-- sfx
self.snd = { sound=Sound.new("audio/sfx/sfx_deathscream_human14.wav"), time=0, delay=0.2, }
end
function SNmes:filter(ent) -- tiny function
return ent.isnme
end
function SNmes:onAdd(ent) -- tiny function
-- print("SNmes:onAdd")
ent.flip = random(100)
if ent.flip > 50 then ent.flip = 1 else ent.flip = -1 end
ent.currlives = ent.totallives
ent.currhealth = ent.totalhealth
ent.washurt = 0
ent.wasbadlyhurt = 0
ent.curractiontimer = ent.actiontimer
end
function SNmes:onRemove(ent) -- tiny function
-- print("SNmes:onRemove")
local function fun()
ent.shield.sprite:setVisible(false)
ent.shield.sprite = nil
if ent.collectible then
if ent.collectible:find("door") then -- append "key" to eid
ent.collectible = "key"..tostring(ent.collectible)
end
--ECollectibles:init(xid, xspritelayer, xpos, xspeed, xdx, xdy)
local el = ECollectibles.new(
ent.collectible, ent.spritelayer,
ent.pos+vector(ent.collbox.w/4, 0*ent.collbox.h),
0.8*8, 6*8, 8*8
)
self.tiny.tworld:addEntity(el)
self.bworld:add(el, el.pos.x, el.pos.y, el.collbox.w, el.collbox.h)
end
Core.yield(1)
end
Core.asyncCall(fun)
self.bworld:remove(ent) -- remove collision box from cbump world here!
end
function SNmes:process(ent, dt) -- tiny function
if ent.isaction1 then -- shoot
ent.isaction1 = false
local projectilespeed = 28*8
local xangle = atan2(self.player1.pos.y-ent.pos.y, self.player1.pos.x-ent.pos.x)
local offset = vector(ent.collbox.w*0.5, ent.collbox.h*0.4)
if ent.eid == 100 then -- shoot straight
if ent.flip == 1 then xangle = ^<0 -- shoot right
else xangle = ^<180 -- shoot left
end
projectilespeed = 26*8
end
local vx, vy = projectilespeed*cos(xangle), projectilespeed*sin(xangle)
--EProjectiles:init(xid, xmass, xangle, xspritelayer, xpos, xvx, xvy, dx, dy, xpersist)
local p = EProjectiles.new(
100, 0.1, xangle, ent.spritelayer, ent.pos+offset, vx, vy, 32*8, 32*8
)
p.body.vx = vx
p.body.vy = vy
self.tiny.tworld:addEntity(p)
self.bworld:add(p, p.pos.x, p.pos.y, p.collbox.w, p.collbox.h)
end
if ent.isaction2 then -- shield
ent.isaction2 = false
if ent.shield then
ent.shield.currtimer = ent.shield.timer
ent.shield.sprite:setVisible(true)
end
end
if ent.isaction3 then -- dash
ent.isaction3 = false
ent.body.currdashtimer = ent.body.dashtimer
ent.body.currdashcooldown = ent.body.dashcooldown
end
-- shield collision
if ent.shield and ent.currlives > 0 then
local function collisionfilter2(item) -- only one param: "item", return true, false or nil
if item.isplayer1 or (item.isprojectile and item.eid == 1) then
return true
end
end
local shiledsprite = ent.shield.sprite -- faster
shiledsprite:setScale(shiledsprite.sx*ent.flip, shiledsprite.sy)
shiledsprite:setPosition(
ent.pos +
vector(ent.collbox.w/2, 0) +
ent.shield.offset*vector(shiledsprite.sx*ent.flip, shiledsprite.sy)
)
local pw, ph = shiledsprite:getWidth(), shiledsprite:getHeight()
--local items, len = world:queryRect(l,t,w,h, filter)
local items, len2 = self.bworld:queryRect(
ent.pos.x+ent.shield.offset.x*ent.flip-pw*0.5+ent.collbox.w*0.5,
ent.pos.y+ent.shield.offset.y-ph*0.5,
pw, ph,
collisionfilter2)
for i = 1, len2 do
local item = items[i]
if shiledsprite:isVisible() then
item.damage = ent.shield.damage
item.isdirty = true
end
end
end
if ent.shield and ent.shield.currtimer > 0 then
ent.shield.currtimer -= 1
if ent.shield.currtimer <= 0 then
ent.shield.sprite:setVisible(false)
end
end
-- hurt
if ent.washurt > 0 and ent.wasbadlyhurt <= 0 and ent.currlives > 0 then -- lose 1 nrg
ent.washurt -= 1
ent.isdirty = false
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
elseif ent.washurt < ent.recovertimer*0.5 then
ent.hitfx:setVisible(false)
end
elseif ent.wasbadlyhurt > 0 and ent.currlives > 0 then -- lose 1 life
ent.wasbadlyhurt -= 1
ent.isdirty = false
if ent.wasbadlyhurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1) -- reset color transform
elseif ent.wasbadlyhurt < ent.recoverbadtimer*0.5 then
ent.hitfx:setVisible(false)
ent.animation.curranim = g_ANIM_STANDUP_R
ent.animation.frame = 0 -- start animation at frame 0
end
end
if ent.body.currdashtimer > 0 then -- invicible while dashing
ent.isdirty = false
end
-- hit
if ent.isdirty then
ent.sprite:setColorTransform(0, 1, 0, 2)
ent.hitfx:setVisible(true)
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y)
ent.spritelayer:addChild(ent.hitfx)
ent.currhealth -= ent.damage
ent.washurt = ent.recovertimer -- timer for a flash effect
ent.animation.curranim = g_ANIM_HURT_R
ent.animation.frame = 0 -- start animation at frame 0
ent.isdirty = false
if ent.currhealth <= 0 then
ent.sprite:setColorTransform(0, 1, 1, 2)
ent.currlives -= 1
if ent.currlives > 0 then
ent.currhealth = ent.totalhealth
end
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for actor to stand back up
ent.animation.curranim = g_ANIM_LOSE1_R
end
end
-- deaded
if ent.currlives <= 0 then
local snd = self.snd -- sfx
local curr = os.timer()
local prev = snd.time
if curr - prev > snd.delay then
local channel = snd.sound:play()
if channel then channel:setVolume(g_sfxvolume*0.01) end
snd.time = curr
end
-- stop all movements
ent.isleft = false
ent.isright = false
ent.isup = false
ent.isdown = false
-- play dead sequence
ent.isdirty = false
ent.washurt = 0 -- ent.recovertimer
ent.wasbadlyhurt = 0 -- ent.recoverbadtimer
if ent.readytoremove then
-- blood
ent.hitfx:setVisible(true)
ent.hitfx:setColorTransform(3, 0, 0, random(1, 3)*0.1) -- red color modulate
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w*0.5, ent.pos.y+ent.h*0.5)
ent.hitfx:setRotation(random(360))
ent.hitfx:setScale(random(5, 8)*0.1)
ent.bgfxlayer:addChild(ent.hitfx)
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(0.5, 0.5, 0.5, 0.5)
self.tiny.tworld:removeEntity(ent) -- sprite is removed in SDrawable
end
end
end
This System deals with the enemies actions:
- 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 action is triggered, we execute it (action1 = shoot, action2 = shield, action3 = dash). Actions will be triggered by an AI System
- when an enemy is hit, we play some sound
- onRemove the enemy is dead, we check if it 'holds' an item (key, coin, ...) and we add the item to the game
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.
sAI.lua
"sAI.lua" in the "_S" folder. The code:
SAI = Core.class()
local random = math.random
function SAI:init(xtiny, xplayer1) -- tiny function
xtiny.processingSystem(self) -- called once on init and every update
self.player1 = xplayer1
end
function SAI:filter(ent) -- tiny function
return ent.ai
end
function SAI:onAdd(ent) -- tiny function
-- abilities
-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
ent.abilities = {}
if ent.body.upspeed > 0 then
ent.abilities[#ent.abilities+1] = 1 -- jump/climb up
end
if ent.eid == 200 then
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
if ent.body.speed > 0 then
ent.abilities[#ent.abilities+1] = 30 -- action3, dash
end
elseif ent.eid == 300 then -- nme boss1
ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
if ent.body.speed > 0 then
ent.abilities[#ent.abilities+1] = 30 -- action3, dash
end
else
ent.abilities[#ent.abilities+1] = 20 -- action2, shield
end
-- for k, v in pairs(ent.abilities) do print(k, v) end
end
function SAI:onRemove(ent) -- tiny function
end
local p1rangetoofarx = myappwidth*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1rangetoofary = myappheight*2 -- 0.7, disable systems to save some CPU, magik XXX
local p1outofrangex = myappwidth*1 -- 0.5, magik XXX
local p1outofrangey = myappheight*1 -- 0.5, magik XXX
local p1actionrange = myappwidth*0.2 -- 3*8, 1*8, myappwidth*0.1, magik XXX
function SAI:process(ent, dt) -- tiny function
if ent.currlives <= 0 then
return
end
local function fun()
-- some flags
ent.doanimate = true -- to save some cpu
-- OUTSIDE VISIBLE RANGE
if (ent.pos.x > self.player1.pos.x + p1rangetoofarx or
ent.pos.x < self.player1.pos.x - p1rangetoofarx) or
(ent.pos.y > self.player1.pos.y + p1rangetoofary or
ent.pos.y < self.player1.pos.y - p1rangetoofary) then
ent.doanimate = false
return
end
-- OUTSIDE ACTION RANGE
if (ent.pos.x > self.player1.pos.x+p1outofrangex or
ent.pos.x < self.player1.pos.x-p1outofrangex) or
(ent.pos.y > self.player1.pos.y+p1outofrangey or
ent.pos.y < self.player1.pos.y-p1outofrangey) then
-- idle
ent.isleft, ent.isright = false, false
ent.isup, ent.isdown = false, false
ent.body.currspeed = ent.body.speed
ent.body.currupspeed = ent.body.upspeed
ent.readyforaction = false
else -- INSIDE ACTION RANGE
-- x
local rnd = random(100)
if rnd > 1 and ent.body.speed > 0 then -- allow nme to stop walking, magik XXX
if ent.pos.x > random(self.player1.pos.x+p1actionrange, self.player1.pos.x+p1outofrangex) then
ent.isleft, ent.isright = true, false
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
elseif ent.pos.x < random(self.player1.pos.x-p1outofrangex, self.player1.pos.x-p1actionrange) then
ent.isleft, ent.isright = false, true
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
end
elseif ent.body.speed == 0 then -- nme always faces player1
if ent.pos.x > self.player1.pos.x then
ent.isleft, ent.isright = true, false
else
ent.isleft, ent.isright = false, true
end
else -- stop moving
ent.isleft, ent.isright = false, false
ent.body.currspeed = ent.body.speed
end
-- no going left or right on stairs/ladders
if ent.isladdercontacts and not (ent.isfloorcontacts or ent.isptpfcontacts) then
ent.isright, ent.isleft = false, false
end
-- bumping on a wall unless isleftofplayer
ent.isleftofplayer = false
if ent.pos.x < self.player1.pos.x then
ent.isleftofplayer = true
end
-- y
-- impulse on top of stairs/ladders and actor is going up
if ent.isladdercontacts and ent.isptpfcontacts and ent.body.vy < 0 then
ent.isup, ent.isdown = true, false
ent.wasup = false
ent.body.currinputbuffer = ent.body.inputbuffer
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
end
-- only act if entity has abilities
if #ent.abilities > 0 then
ent.readyforaction = true
end
end
-- ACTION
if ent.readyforaction then
ent.curractiontimer -= 1
-- don't let the entity get stuck in place when in range
if ent.body.vx == 0 and ent.body.speed > 0 then
if ent.pos.x > self.player1.pos.x then ent.isleft, ent.isright = true, false
else ent.isleft, ent.isright = false, true
end
ent.body.currspeed = ent.body.speed*random(8, 12)*0.1 -- magik XXX
end
if ent.curractiontimer < 0 then
-- actor above player1, go down
if ent.pos.y < self.player1.pos.y and ent.body.upspeed > 0 then
if ent.isptpfcontacts then
local rnd = random(100)
if rnd > 30 then -- 80, magik XXX
ent.isup, ent.isdown = false, true
ent.wasdown = false
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
end
elseif ent.body.upspeed > 0 then -- ladders/stairs
ent.isup, ent.isdown = false, true
ent.wasdown = false
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
end
end
-- actor below player1, jump
if ent.pos.y > self.player1.pos.y + 8 and ent.body.upspeed > 0 then
local rnd = random(100)
if rnd > 40 then -- 80, magik XXX
ent.isup, ent.isdown = true, false
ent.wasup = false
ent.body.currinputbuffer = ent.body.inputbuffer
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
end
end
-- pick a random available action
local rndaction = ent.abilities[random(#ent.abilities)]
-- movements
if rndaction == 1 and ent.body.upspeed > 0 then -- jump
local rnd = random(100)
if rnd > 70 then -- 80, jump
ent.isup, ent.isdown = true, false
ent.wasup = false
ent.body.currinputbuffer = ent.body.inputbuffer
ent.body.currupspeed = ent.body.upspeed*random(8, 12)*0.1 -- magik XXX
end
end
-- actions
local rnd = random(100)
if rndaction == 10 then -- shoot
ent.isaction1 = false
if rnd > 99 then -- rate
ent.isaction1 = true
end
elseif rndaction == 20 then -- shield
ent.isaction2 = false
if rnd > 98 then -- rate
ent.isaction2 = true
end
elseif rndaction == 30 then -- dash
ent.isaction3 = false
if rnd > 95 then -- rate
ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
ent.isaction3 = true
end
end
end
-- extra attack on jumping peak
if ent.body.vy > 0 and ent.body.vy < 30 and ent.body.upspeed > 0 then -- magik XXX
if ent.curractiontimer < 0 then
local rndaction = ent.abilities[random(#ent.abilities)]
local rnd = random(100)
if rnd > 70 then -- rate
if rndaction == 10 then -- shoot
ent.isaction1 = true
elseif rndaction == 20 then -- shield
ent.isaction2 = true
end
else -- dash/shield
local rnd2 = random(100)
if rnd2 > 30 then
if rndaction == 30 then -- dash
ent.isaction3 = true
end
else
ent.body.currspeed = ent.body.speed*0.5 -- magik XXX
if rndaction == 20 then -- shield
ent.isaction2 = true
end
end
end
ent.curractiontimer = ent.actiontimer
end
end
end
Core.yield(1)
end
Core.asyncCall(fun) -- profiler seems to be faster without asyncCall (because of pairs traversing?)
end
This System controls all the entities with an artificial intelligence (ai) id:
- runs once on init and every game loop (process)
- onAdd() we assign various abilities depending on the type of enemy id
-- abilities
ent.abilities = {}
-- ent.abilities[#ent.abilities+1] = 1 -- jump/climb
-- ent.abilities[#ent.abilities+1] = 10 -- action1, shoot
-- ent.abilities[#ent.abilities+1] = 20 -- action2, shield
-- ent.abilities[#ent.abilities+1] = 30 -- action3, dash
- process() when the enemy is outside the visible screen we don't process it
- process() when the enemy is in action range with the player1 we randomly trigger an action (ability)
sAnimation.lua
The animation System. "sAnimation.lua" in the "_S" folder. The code:
SAnimation = Core.class()
function SAnimation:init(xtiny)
xtiny.processingSystem(self) -- called once on init and every frames
self.sndstepgrass = { sound=Sound.new("audio/sfx/footstep/Grass02.wav"), time=0, delay=0.2, }
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
--@native -- new in Gideros
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[anim.curranim] then anim.curranim else g_ANIM_DEFAULT
-- print("checkanim", checkanim)
-- print("#anim.anims[checkanim]", #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_HURT_R or 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_JUMPUP_R or checkanim == g_ANIM_JUMPDOWN_R then
if anim.frame > #anim.anims[checkanim] then
anim.frame = #anim.anims[checkanim]
end
else -- any looping animation
-- player1 steps sound fx
if ent.isplayer1 then
if (anim.curranim == g_ANIM_WALK_R or anim.curranim == g_ANIM_RUN_R) and
(anim.frame == 3 or anim.frame == 7) then
local snd = self.sndstepgrass
local curr = os.timer()
local prev = snd.time
if curr - prev > snd.delay then
local channel = snd.sound:play()
if channel then channel:setVolume(g_sfxvolume*0.01) end
snd.time = curr
end
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
This is the System responsible for all the animations in the game:
- runs once on init and every game loop (process)
- it executes an animation or falls back to the default animation (g_ANIM_DEFAULT)
- it only runs when the doanimate flag is set to true
- then it increases the frame based on a timer
- on certain animations, the frame loops back to 1
- on other animations, the frame will stop at the end of the animation
- we can also trigger events on specific frame number (eg. play sound)
Next?
Systems are fairly straight forward and fairly short for what they achieve.
We have covered the first set of systems, we have a couple more to add.
Prev.: Tuto tiny-ecs 2d platformer Part 8 more entities
Next: Tuto tiny-ecs 2d platformer Part 10 Systems 2
Tutorial - tiny-ecs 2d platformer