Difference between revisions of "Tuto tiny-ecs beatemup Part 9 Systems"
(wip) |
(wip) |
||
Line 54: | Line 54: | ||
* runs only once when it is called | * runs only once when it is called | ||
* affects only entities which have a spritelayer variable (id) '''and''' a sprite variable (id) | * 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 | + | * 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 | + | * when an Entity is removed from tiny-ecs '''World''', the '''System''' removes the Entity from its Sprite layer |
− | + | 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: | ||
<syntaxhighlight lang="lua"> | <syntaxhighlight lang="lua"> | ||
+ | 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 | ||
+ | self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e) | ||
+ | if ent.currlives > 0 then | ||
+ | if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = true end | ||
+ | if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = true end | ||
+ | if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = true end | ||
+ | if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = true end | ||
+ | -- ACTIONS: | ||
+ | -- isactionpunch1, isactionpunch2, isactionjumppunch1, | ||
+ | -- isactionkick1, isactionkick2, isactionjumpkick1, | ||
+ | -- isactionjump1 | ||
+ | if e.keyCode == g_keyaction1 then | ||
+ | ent.animation.frame = 0 | ||
+ | ent.isactionpunch1 = true | ||
+ | elseif e.keyCode == g_keyaction2 then | ||
+ | ent.animation.frame = 0 | ||
+ | ent.isactionkick1 = true | ||
+ | end | ||
+ | if e.keyCode == g_keyaction3 then | ||
+ | if ent.body.isonfloor then | ||
+ | ent.animation.frame = 0 | ||
+ | ent.positionystart = ent.pos.y | ||
+ | ent.body.isonfloor = false | ||
+ | ent.body.isgoingup = true | ||
+ | ent.isactionjump1 = 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 ent.isleft = false end | ||
+ | if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = false end | ||
+ | if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = false end | ||
+ | if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = false end | ||
+ | -- if e.keyCode == g_keyaction1 then ent.isactionpunch1 = false end | ||
+ | -- if e.keyCode == g_keyaction2 then ent.isactionkick1 = false end | ||
+ | -- if e.keyCode == g_keyaction3 then end | ||
+ | end | ||
+ | end) | ||
+ | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 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 ''sDynamicBodies'' System we will add. | ||
+ | |||
+ | == sPlayer1.lua == | ||
+ | "'''sPlayer1.lua'''" in the '''"_S"''' folder. The code: | ||
+ | <syntaxhighlight lang="lua"> | ||
+ | SPlayer1 = Core.class() | ||
+ | |||
+ | function SPlayer1:init(xtiny, 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 | ||
+ | -- fx | ||
+ | self.camera = xcamera -- camera shake | ||
+ | -- sfx | ||
+ | self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav") | ||
+ | self.channel = self.snd:play(0, false, true) | ||
+ | end | ||
+ | |||
+ | function SPlayer1:filter(ent) -- tiny function | ||
+ | return ent.isplayer1 | ||
+ | end | ||
+ | |||
+ | function SPlayer1:onAdd(ent) -- tiny function | ||
+ | end | ||
+ | |||
+ | function SPlayer1:onRemove(ent) -- tiny function | ||
+ | end | ||
+ | |||
+ | local resetanim = true | ||
+ | function SPlayer1:process(ent, dt) -- tiny function | ||
+ | -- hurt fx | ||
+ | if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) then | ||
+ | ent.washurt -= 1 | ||
+ | ent.animation.curranim = g_ANIM_HURT_R | ||
+ | if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end | ||
+ | if ent.washurt <= 0 then | ||
+ | ent.sprite:setColorTransform(1, 1, 1, 1) | ||
+ | self.camera:setZoom(1) -- zoom | ||
+ | end | ||
+ | elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 then | ||
+ | ent.hitfx:setVisible(false) | ||
+ | ent.wasbadlyhurt -= 1 | ||
+ | ent.animation.curranim = g_ANIM_LOSE1_R | ||
+ | if ent.wasbadlyhurt < ent.recoverbadtimer/2 then | ||
+ | if resetanim then | ||
+ | resetanim = false | ||
+ | ent.animation.frame = 0 | ||
+ | end | ||
+ | ent.animation.curranim = g_ANIM_STANDUP_R | ||
+ | end | ||
+ | if ent.wasbadlyhurt <= 0 then | ||
+ | ent.sprite:setColorTransform(1, 1, 1, 1) | ||
+ | self.camera:setZoom(1) -- zoom | ||
+ | resetanim = true | ||
+ | end | ||
+ | end | ||
+ | if ent.isdirty then -- hit | ||
+ | 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 | ||
+ | self.channel = self.snd:play() | ||
+ | if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end | ||
+ | ent.hitfx:setVisible(true) | ||
+ | ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2+(ent.headhurtbox.x*ent.flip), ent.pos.y+ent.headhurtbox.y-ent.headhurtbox.h/2) | ||
+ | ent.spritelayer:addChild(ent.hitfx) | ||
+ | ent.currhealth -= ent.damage | ||
+ | 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.sprite:setColorTransform(2, 0, 0, 2) -- the flash effect (a bright red color) | ||
+ | ent.isdirty = false | ||
+ | self.camera:shake(0.6, 16) -- (duration, distance), you choose | ||
+ | self.camera:setZoom(1.2) -- zoom | ||
+ | if ent.currhealth <= 0 then | ||
+ | ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up | ||
+ | self.camera:shake(0.8, 64) -- (duration, distance), you choose | ||
+ | ent.currlives -= 1 | ||
+ | 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 | ||
+ | end | ||
+ | end | ||
+ | if ent.currlives <= 0 then -- deaded | ||
+ | -- stop all movements | ||
+ | ent.isleft = false | ||
+ | ent.isright = false | ||
+ | ent.isup = false | ||
+ | ent.isdown = false | ||
+ | -- play dead sequence | ||
+ | ent.isdirty = false | ||
+ | resetanim = false | ||
+ | ent.washurt = ent.recovertimer | ||
+ | ent.wasbadlyhurt = ent.recoverbadtimer | ||
+ | ent.animation.curranim = g_ANIM_LOSE1_R | ||
+ | ent.sprite:setColorTransform(255*0.5/255, 255*0.5/255, 255*0.5/255, 1) | ||
+ | self.camera:setZoom(1) -- zoom | ||
+ | ent.animation.bmp:setY(ent.animation.bmp:getY()-1) | ||
+ | if ent.animation.bmp:getY() < -200 then -- you choose | ||
+ | switchToScene(LevelX.new()) | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | </syntaxhighlight> | ||
− | + | This System deals with the player1 being hit or killed: | |
+ | * 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 | ||
+ | * 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 | ||
== Next? == | == Next? == |
Revision as of 21:27, 21 November 2024
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 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
That looks scary but worry not we won't use all the callback functions :-)
To put it simple a System manipulates entities. Let's see our first System.
sDrawable.lua
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.spritelayer:addChild(ent.sprite)
end
function SDrawable:onRemove(ent) -- tiny function
-- print("SDrawable:onRemove(ent)")
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
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
self.player1inputlayer:addEventListener(Event.KEY_DOWN, function(e)
if ent.currlives > 0 then
if e.keyCode == KeyCode.LEFT or e.keyCode == g_keyleft then ent.isleft = true end
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = true end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = true end
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = true end
-- ACTIONS:
-- isactionpunch1, isactionpunch2, isactionjumppunch1,
-- isactionkick1, isactionkick2, isactionjumpkick1,
-- isactionjump1
if e.keyCode == g_keyaction1 then
ent.animation.frame = 0
ent.isactionpunch1 = true
elseif e.keyCode == g_keyaction2 then
ent.animation.frame = 0
ent.isactionkick1 = true
end
if e.keyCode == g_keyaction3 then
if ent.body.isonfloor then
ent.animation.frame = 0
ent.positionystart = ent.pos.y
ent.body.isonfloor = false
ent.body.isgoingup = true
ent.isactionjump1 = 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 ent.isleft = false end
if e.keyCode == KeyCode.RIGHT or e.keyCode == g_keyright then ent.isright = false end
if e.keyCode == KeyCode.UP or e.keyCode == g_keyup then ent.isup = false end
if e.keyCode == KeyCode.DOWN or e.keyCode == g_keydown then ent.isdown = false end
-- if e.keyCode == g_keyaction1 then ent.isactionpunch1 = false end
-- if e.keyCode == g_keyaction2 then ent.isactionkick1 = false end
-- if e.keyCode == g_keyaction3 then 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 sDynamicBodies System we will add.
sPlayer1.lua
"sPlayer1.lua" in the "_S" folder. The code:
SPlayer1 = Core.class()
function SPlayer1:init(xtiny, 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
-- fx
self.camera = xcamera -- camera shake
-- sfx
self.snd = Sound.new("audio/sfx/sfx_deathscream_human14.wav")
self.channel = self.snd:play(0, false, true)
end
function SPlayer1:filter(ent) -- tiny function
return ent.isplayer1
end
function SPlayer1:onAdd(ent) -- tiny function
end
function SPlayer1:onRemove(ent) -- tiny function
end
local resetanim = true
function SPlayer1:process(ent, dt) -- tiny function
-- hurt fx
if ent.washurt and ent.washurt > 0 and not (ent.wasbadlyhurt and ent.wasbadlyhurt > 0) then
ent.washurt -= 1
ent.animation.curranim = g_ANIM_HURT_R
if ent.washurt < ent.recovertimer*0.5 then ent.hitfx:setVisible(false) end
if ent.washurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1)
self.camera:setZoom(1) -- zoom
end
elseif ent.wasbadlyhurt and ent.wasbadlyhurt > 0 then
ent.hitfx:setVisible(false)
ent.wasbadlyhurt -= 1
ent.animation.curranim = g_ANIM_LOSE1_R
if ent.wasbadlyhurt < ent.recoverbadtimer/2 then
if resetanim then
resetanim = false
ent.animation.frame = 0
end
ent.animation.curranim = g_ANIM_STANDUP_R
end
if ent.wasbadlyhurt <= 0 then
ent.sprite:setColorTransform(1, 1, 1, 1)
self.camera:setZoom(1) -- zoom
resetanim = true
end
end
if ent.isdirty then -- hit
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
self.channel = self.snd:play()
if self.channel then self.channel:setVolume(g_sfxvolume*0.01) end
ent.hitfx:setVisible(true)
ent.hitfx:setPosition(ent.pos.x+ent.collbox.w/2+(ent.headhurtbox.x*ent.flip), ent.pos.y+ent.headhurtbox.y-ent.headhurtbox.h/2)
ent.spritelayer:addChild(ent.hitfx)
ent.currhealth -= ent.damage
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.sprite:setColorTransform(2, 0, 0, 2) -- the flash effect (a bright red color)
ent.isdirty = false
self.camera:shake(0.6, 16) -- (duration, distance), you choose
self.camera:setZoom(1.2) -- zoom
if ent.currhealth <= 0 then
ent.wasbadlyhurt = ent.recoverbadtimer -- timer for player1 to stand back up
self.camera:shake(0.8, 64) -- (duration, distance), you choose
ent.currlives -= 1
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
end
end
if ent.currlives <= 0 then -- deaded
-- stop all movements
ent.isleft = false
ent.isright = false
ent.isup = false
ent.isdown = false
-- play dead sequence
ent.isdirty = false
resetanim = false
ent.washurt = ent.recovertimer
ent.wasbadlyhurt = ent.recoverbadtimer
ent.animation.curranim = g_ANIM_LOSE1_R
ent.sprite:setColorTransform(255*0.5/255, 255*0.5/255, 255*0.5/255, 1)
self.camera:setZoom(1) -- zoom
ent.animation.bmp:setY(ent.animation.bmp:getY()-1)
if ent.animation.bmp:getY() < -200 then -- you choose
switchToScene(LevelX.new())
end
end
end
This System deals with the player1 being hit or killed:
- 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
- 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
Next?
The time has come to tackle the systems. I will try to make it easy :-)
Prev.: Tuto tiny-ecs beatemup Part 8 Breakables
Next: Tuto tiny-ecs beatemup Part 10 XXX