Difference between revisions of "Tuto tiny-ecs 2d platformer Part 5 ePlayer1"

From GiderosMobile
(wip)
 
 
Line 14: Line 14:
 
self.isplayer1 = true
 
self.isplayer1 = true
 
self.doanimate = true -- to save some cpu
 
self.doanimate = true -- to save some cpu
 +
self.ispersistent = true -- keep sprite visible when dead
 
-- sprite layers
 
-- sprite layers
 
self.spritelayer = xspritelayer
 
self.spritelayer = xspritelayer
Line 19: Line 20:
 
-- params
 
-- params
 
self.pos = xpos
 
self.pos = xpos
self.positionystart = 0
+
self.sx = 1 -- 0.96, 0.8, 1.05, 1.1, 1.2
self.sx = 1
 
 
self.sy = self.sx
 
self.sy = self.sx
 
self.flip = 1
 
self.flip = 1
 
self.totallives = 3
 
self.totallives = 3
self.totalhealth = 10
+
self.totalhealth = 3
self.currjumps = 5
 
 
if g_difficulty == 0 then -- easy
 
if g_difficulty == 0 then -- easy
self.totallives = 5
+
self.totallives *= 2
self.totalhealth = 20
+
self.totalhealth *= 2
self.currjumps = 8
 
elseif g_difficulty == 2 then -- hard
 
self.currjumps = 3
 
 
end
 
end
 
self.currlives = self.totallives
 
self.currlives = self.totallives
Line 38: Line 34:
 
self.washurt = 0
 
self.washurt = 0
 
self.wasbadlyhurt = 0
 
self.wasbadlyhurt = 0
self.recovertimer = 30
+
self.recovertimer = 20 -- 30
self.recoverbadtimer = 90
+
self.recoverbadtimer = 60 -- 90
 
if g_difficulty == 0 then -- easy
 
if g_difficulty == 0 then -- easy
 
self.recovertimer *= 2
 
self.recovertimer *= 2
Line 47: Line 43:
 
self.recoverbadtimer *= 0.5
 
self.recoverbadtimer *= 0.5
 
end
 
end
self.ispaused = false -- 'P' key for pausing the game
+
self.hitfx = Bitmap.new(Texture.new("gfx/fxs/2.png"))
self.hitfx = Bitmap.new(Texture.new("gfx/fx/2.png"))
 
 
self.hitfx:setAnchorPoint(0.5, 0.5)
 
self.hitfx:setAnchorPoint(0.5, 0.5)
 
-- COMPONENTS
 
-- COMPONENTS
-- ANIMATION: CAnimation:init(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
+
-- ANIMATION
local texpath = "gfx/player1/mh_blue_haired2m_0130.png"
+
local anims = {} -- table to hold actor animations
local framerate = 1/10
+
local animsimgs = {} -- table to hold actor animations images
self.animation = CAnimation.new(texpath, 12, 11, framerate, 0, 0, self.sx, self.sy)
+
-- CAnimation:init(xanimspeed)
 +
local framerate = 1/10 -- 1/14, 1/18
 +
self.animation = CAnimation.new(framerate)
 +
-- CAnimation:cutSpritesheet(xspritesheetpath, xcols, xrows, xanimsimgs, xoffx, xoffy, sx, sy)
 +
local texpath = "gfx/player1/Matt_0001.png"
 +
self.animation:cutSpritesheet(texpath, 9, 6, animsimgs, 0, 1, self.sx, self.sy)
 +
-- 1st set of animations: CAnimation:createAnim(xanims, xanimname, xanimsimgs, xtable, xstart, xfinish)
 +
self.animation:createAnim(anims, g_ANIM_DEFAULT, animsimgs, nil, 1, 15)
 +
self.animation:createAnim(anims, g_ANIM_IDLE_R, animsimgs, nil, 1, 15) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_RUN_R, animsimgs, nil, 16, 22) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_JUMPUP_R, animsimgs, nil, 23, 23) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_JUMPDOWN_R, animsimgs, nil, 23, 25) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_LADDER_IDLE_R, animsimgs, nil, 1, 15) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_LADDER_UP_R, animsimgs, nil, 16, 22) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_LADDER_DOWN_R, animsimgs, nil, 16, 22) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_WALL_IDLE_R, animsimgs, nil, 33, 33) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_WALL_UP_R, animsimgs, nil, 26, 37) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_WALL_DOWN_R, animsimgs, nil, 43, 54) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_HURT_R, animsimgs, { 38, 39, 38, 39, 1, }) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_LOSE1_R, animsimgs, nil, 38, 42) -- fluid is best
 +
self.animation:createAnim(anims, g_ANIM_STANDUP_R, animsimgs, { 42, 41, 40, 39, 1, }) -- fluid is best
 +
self.animation.anims = anims
 
self.sprite = self.animation.sprite
 
self.sprite = self.animation.sprite
 
self.animation.sprite = nil -- free some memory
 
self.animation.sprite = nil -- free some memory
 
self.w, self.h = self.sprite:getWidth(), self.sprite:getHeight() -- with applied scale
 
self.w, self.h = self.sprite:getWidth(), self.sprite:getHeight() -- with applied scale
-- print("player1 size: ", self.w, self.h)
+
print("player1 size (scaled): ", self.w, self.h)
-- create basics animations: CAnimation:createAnim(xanimname, xstart, xfinish)
+
-- BODY: CBody:init(xmass, xspeed, xupspeed, xextra)
self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
+
self.body = CBody.new(1, 18*8, 68*8, true) -- (1, 22*8, 70*8, true)
self.animation:createAnim(g_ANIM_IDLE_R, 1, 15) -- fluid is best
 
self.animation:createAnim(g_ANIM_WALK_R, 16, 26) -- fluid is best
 
self.animation:createAnim(g_ANIM_JUMP1_R, 74, 76) -- fluid is best
 
self.animation:createAnim(g_ANIM_HURT_R, 90, 100) -- fluid is best
 
self.animation:createAnim(g_ANIM_STANDUP_R, 103, 113) -- fluid is best
 
self.animation:createAnim(g_ANIM_LOSE1_R, 113, 124) -- fluid is best
 
-- BODY: CBody:init(xspeed, xjumpspeed)
 
self.body = CBody.new(192*32, 0.65) -- (192*32, 1), xspeed, xjumpspeed
 
 
-- COLLISION BOX: CCollisionBox:init(xcollwidth, xcollheight)
 
-- COLLISION BOX: CCollisionBox:init(xcollwidth, xcollheight)
local collw, collh = self.w*0.4, 8*self.sy
+
local collw, collh = (self.w*0.3)//1, (self.h*0.75)//1 -- must be round numbers for cbump physics!
 
self.collbox = CCollisionBox.new(collw, collh)
 
self.collbox = CCollisionBox.new(collw, collh)
-- HURT BOX
+
-- shield
-- head hurt box w & h
+
self.shield = {}
local hhbw, hhbh = self.w*0.25, self.h*0.3
+
-- self.shield.sprite = Bitmap.new(Texture.new("gfx/fxs/Husky_0001.png"))
self.headhurtbox = {
+
self.shield.sprite = Pixel.new(0xffaa00, 0.75, 8, collh+4)
isactive=false,
+
self.shield.sprite.sx = 1
x=-2*self.sx,
+
self.shield.sprite.sy = self.shield.sprite.sx
y=0*self.sy-self.h/2-self.collbox.h*2,
+
self.shield.sprite:setScale(self.shield.sprite.sx, self.shield.sprite.sy)
w=hhbw,
+
self.shield.sprite:setAlpha(0.8)
h=hhbh,
+
self.shield.sprite:setAnchorPoint(0.5, 0.5)
}
+
self.spritelayer:addChild(self.shield.sprite)
-- spine hurt box w & h
+
self.shield.offset = vector(collw, (collh-4)*0.5) -- (5*8, -1*8)
local shbw, shbh = self.w*0.35, self.h*0.4
+
self.shield.timer = 3*8 -- 2*8
self.spinehurtbox = {
+
self.shield.currtimer = self.shield.timer
isactive=false,
+
self.shield.damage = 0.5
x=-0*self.sx,
 
y=0*self.sy-shbh/2+self.collbox.h/2,
 
w=shbw,
 
h=shbh,
 
}
 
-- create attacks animations: CAnimation:createAnim(xanimname, xstart, xfinish)
 
-- self.animation:createAnim(g_ANIM_PUNCH_ATTACK1_R, 50, 51) -- 28, 31, no or low anticipation / quick hit / no or low overhead is best
 
self.animation.anims[g_ANIM_PUNCH_ATTACK1_R] = {}
 
self.animation.anims[g_ANIM_PUNCH_ATTACK1_R][1] = self.animation.myanimsimgs[49]
 
self.animation.anims[g_ANIM_PUNCH_ATTACK1_R][2] = self.animation.myanimsimgs[52]
 
self.animation.anims[g_ANIM_PUNCH_ATTACK1_R][3] = self.animation.myanimsimgs[54]
 
self.animation:createAnim(g_ANIM_PUNCH_ATTACK2_R, 50, 54) -- low or mid anticipation / quick hit / low or mid overhead is best
 
-- self.animation:createAnim(g_ANIM_KICK_ATTACK1_R, 62, 63) -- 35, 41, no or low anticipation / quick hit / no or low overhead is best
 
self.animation.anims[g_ANIM_KICK_ATTACK1_R] = {}
 
self.animation.anims[g_ANIM_KICK_ATTACK1_R][1] = self.animation.myanimsimgs[62]
 
self.animation.anims[g_ANIM_KICK_ATTACK1_R][2] = self.animation.myanimsimgs[64]
 
self.animation.anims[g_ANIM_KICK_ATTACK1_R][3] = self.animation.myanimsimgs[67]
 
self.animation:createAnim(g_ANIM_KICK_ATTACK2_R, 62, 68) -- low or mid anticipation / quick hit / low or mid overhead is best
 
-- self.animation:createAnim(g_ANIM_PUNCHJUMP_ATTACK1_R, 75, 82) -- low or mid anticipation / quick hit / low or mid overhead is best
 
self.animation.anims[g_ANIM_PUNCHJUMP_ATTACK1_R] = {}
 
self.animation.anims[g_ANIM_PUNCHJUMP_ATTACK1_R][1] = self.animation.myanimsimgs[78]
 
self.animation.anims[g_ANIM_PUNCHJUMP_ATTACK1_R][2] = self.animation.myanimsimgs[80]
 
self.animation.anims[g_ANIM_PUNCHJUMP_ATTACK1_R][3] = self.animation.myanimsimgs[82]
 
-- self.animation:createAnim(g_ANIM_KICKJUMP_ATTACK1_R, 83, 88) -- low or mid anticipation / quick hit / low or mid overhead is best
 
self.animation.anims[g_ANIM_KICKJUMP_ATTACK1_R] = {}
 
self.animation.anims[g_ANIM_KICKJUMP_ATTACK1_R][1] = self.animation.myanimsimgs[84]
 
self.animation.anims[g_ANIM_KICKJUMP_ATTACK1_R][2] = self.animation.myanimsimgs[86]
 
-- clean up
 
self.animation.myanimsimgs = nil
 
-- hit box
 
self.headhitboxattack1 = { -- g_ANIM_PUNCH_ATTACK1_R
 
isactive=false,
 
hitstartframe=2,
 
hitendframe=3,
 
damage=1,
 
x=self.collbox.w*0.6,
 
y=-self.h*0.6+collh*0.5,
 
w=20*self.sx,
 
h=32*self.sy,
 
}
 
self.headhitboxattack2 = { -- g_ANIM_PUNCH_ATTACK2_R X
 
isactive=false,
 
hitstartframe=1,
 
hitendframe=4,
 
damage=2,
 
x=self.collbox.w*0.75,
 
y=-self.h*0.65+collh*0.5,
 
w=32*self.sx,
 
h=32*self.sy,
 
}
 
self.spinehitboxattack1 = { -- g_ANIM_KICK_ATTACK1_R
 
isactive=false,
 
hitstartframe=2,
 
hitendframe=3,
 
damage=1,
 
x=self.collbox.w*0.7,
 
y=-self.h*0.25+collh*0.5,
 
w=40*self.sx,
 
h=self.h*0.5,
 
}
 
self.spinehitboxattack2 = { -- g_ANIM_KICK_ATTACK2_R
 
isactive=false,
 
hitstartframe=2,
 
hitendframe=4,
 
damage=2,
 
x=self.collbox.w*0.8,
 
y=-self.h*0.25+collh*0.5,
 
w=32*self.sx,
 
h=self.h*0.5,
 
}
 
self.headhitboxjattack1 = { -- g_ANIM_PUNCHJUMP_ATTACK1_R
 
isactive=false,
 
hitstartframe=6,
 
hitendframe=8,
 
damage=3,
 
x=self.collbox.w*0.7,
 
y=-self.h*0.5+collh*0.5,
 
w=40*self.sx,
 
h=self.h*0.5,
 
}
 
self.spinehitboxjattack1 = { -- g_ANIM_KICKJUMP_ATTACK1_R
 
isactive=false,
 
hitstartframe=3,
 
hitendframe=5,
 
damage=3,
 
x=self.collbox.w*0.5,
 
y=-self.h*0.25+collh*0.5,
 
w=64*self.sx,
 
h=self.h*0.5,
 
}
 
-- SHADOW: CShadow:init(xparentw, xshadowsx, xshadowsy)
 
self.shadow = CShadow.new(self.w*0.6)
 
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
Line 185: Line 101:
  
 
=== ids ===
 
=== ids ===
Ids are used as filters by the ECS systems. Based on an Entity id, a System will process it or not. Here we have an id to tell this is the player1 Entity and an id telling this sprite is animated.
+
Ids are used as filters by the ECS systems. Based on an Entity id, a System will process it or not. Here we have an id to tell this is the player1 Entity, an id telling this sprite is animated, ...
  
 
  '''All the player1 variables below can be used as ids as well, should we want to narrow down a System filter'''
 
  '''All the player1 variables below can be used as ids as well, should we want to narrow down a System filter'''
Line 193: Line 109:
 
* the sprite layers the actor lives in
 
* the sprite layers the actor lives in
 
* the actor position and scale
 
* the actor position and scale
* ''positionystart'' the starting position on the y axis before the actor performs a jump
 
 
* flip to indicate the direction the actor is facing
 
* flip to indicate the direction the actor is facing
 
* the number of lives, health, jumps, ...
 
* the number of lives, health, jumps, ...
Line 200: Line 115:
 
When the actor is hit, we give it some time to recover. During this time the actor is invincible.
 
When the actor is hit, we give it some time to recover. During this time the actor is invincible.
  
=== my mistake! ===
+
=== '''fx''' ===
I added a ''self.ispaused'' variable which is completly useless, you can safely delete it :-)
+
''self.hitfx'' is one of those sprite we add to the fxlayer when the player1 is hit by another actor.
 
 
''self.hitfx'' is one of those fx we add to the fxlayer when the player1 successfully hits another actor.
 
  
 
== COMPONENTS ==
 
== COMPONENTS ==
Line 213: Line 126:
 
CAnimation = Core.class()
 
CAnimation = Core.class()
  
function CAnimation:init(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
+
function CAnimation:init(xanimspeed)
 
-- animation
 
-- animation
 
self.curranim = g_ANIM_DEFAULT
 
self.curranim = g_ANIM_DEFAULT
Line 219: Line 132:
 
self.animspeed = xanimspeed
 
self.animspeed = xanimspeed
 
self.animtimer = self.animspeed
 
self.animtimer = self.animspeed
 +
end
 +
 +
function CAnimation:cutSpritesheet(xspritesheetpath, xcols, xrows, xanimsimgs, xoffx, xoffy, sx, sy)
 
-- retrieve all anims in texture
 
-- retrieve all anims in texture
 
local myanimstex = Texture.new(xspritesheetpath)
 
local myanimstex = Texture.new(xspritesheetpath)
local cellw = myanimstex:getWidth() / xcols
+
local cellw = myanimstex:getWidth()/xcols
local cellh = myanimstex:getHeight() / xrows
+
local cellh = myanimstex:getHeight()/xrows
self.myanimsimgs = {}
 
local myanimstexregion
 
 
for r = 1, xrows do
 
for r = 1, xrows do
 
for c = 1, xcols do
 
for c = 1, xcols do
myanimstexregion = TextureRegion.new(myanimstex, (c - 1) * cellw, (r - 1) * cellh, cellw, cellh)
+
local myanimstexregion = TextureRegion.new(
self.myanimsimgs[#self.myanimsimgs + 1] = myanimstexregion
+
myanimstex, (c-1)*cellw, (r-1)*cellh, cellw, cellh
 +
)
 +
xanimsimgs[#xanimsimgs + 1] = myanimstexregion
 
end
 
end
 
end
 
end
-- anims table ("walk", "jump", "shoot", ...)
 
self.anims = {}
 
 
-- the bitmap
 
-- the bitmap
self.bmp = Bitmap.new(self.myanimsimgs[1]) -- starting bmp texture
+
self.bmp = Bitmap.new(xanimsimgs[1]) -- starting bmp texture
 
self.bmp:setScale(sx, sy) -- scale!
 
self.bmp:setScale(sx, sy) -- scale!
 
self.bmp:setAnchorPoint(0.5, 0.5) -- we will flip the bitmap
 
self.bmp:setAnchorPoint(0.5, 0.5) -- we will flip the bitmap
Line 242: Line 156:
 
self.sprite = Sprite.new()
 
self.sprite = Sprite.new()
 
self.sprite:addChild(self.bmp)
 
self.sprite:addChild(self.bmp)
 +
self.bmp:setColorTransform(8*32/255, 8*32/255, 8*32/255, 9*32/255)
 
end
 
end
  
function CAnimation:createAnim(xanimname, xstart, xfinish)
+
function CAnimation:createAnim(xanims, xanimname, xanimsimgs, xtable, xstart, xfinish)
self.anims[xanimname] = {}
+
xanims[xanimname] = {}
for i = xstart, xfinish do
+
if xtable and #xtable > 0 then
self.anims[xanimname][#self.anims[xanimname]+1] = self.myanimsimgs[i]
+
for i = 1, #xtable do
 +
xanims[xanimname][#xanims[xanimname]+1] = xanimsimgs[xtable[i]]
 +
end
 +
else
 +
for i = xstart, xfinish do
 +
xanims[xanimname][#xanims[xanimname]+1] = xanimsimgs[i]
 +
end
 
end
 
end
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
The parameters are:
+
In the init function we simply indicate the animation speed, then we initialize the basics variables.
* xspritesheetpath: the path to the player1 spritesheet
 
* xcols, xrows: the spritesheet number of columns and rows
 
* xanimspeed: the animation speed in ms
 
* xoffx, xoffy: an offset in case we need to
 
* sx, sy: the scale of the sprite
 
  
The Animation Component cuts the spritesheet into single images and puts them in the ''self.myanimsimgs'' table. We then use ''self.myanimsimgs'' and pick the first image as our player1 bitmap. The ''self.myanimsimgs'' table is also used to create the animations with the '''createAnim''' function as we see next.
+
Then the Animation Component cuts the spritesheet into single images.
 +
 
 +
Finally the Animation Component creates the animations table with the animation name and images.
  
 
==== animations ====
 
==== animations ====
Back to the '''"ePlayer1.lua"''' code we create its animations.
+
Back to the '''"ePlayer1.lua"''' code we create the animations.
 +
 
 +
To add images to an animation you can pass a table with the spritesheet images cell number:
 +
<syntaxhighlight lang="lua">
 +
self.animation:createAnim(anims, g_ANIM_STANDUP_R, animsimgs, { 42, 41, 40, 39, 1, }, nil)
 +
</syntaxhighlight>
 +
 
 +
Or you assign the starting image and the ending image in the spritesheets:
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
+
self.animation:createAnim(anims, g_ANIM_DEFAULT, animsimgs, nil, 1, 15)
self.animation:createAnim(g_ANIM_IDLE_R, 1, 15) -- fluid is best
+
self.animation:createAnim(anims, g_ANIM_IDLE_R, animsimgs, nil, 1, 15)
self.animation:createAnim(g_ANIM_WALK_R, 16, 26) -- fluid is best
 
self.animation:createAnim(g_ANIM_JUMP1_R, 74, 76) -- fluid is best
 
...
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
We first create the basics animations: idle, walk, jump. We also have extra animations: hurt, stand up and lose. Here it is better to have smooth animations so we have quite a few images. The g_ANIM_DEFAULT animation was used when building the game, we shouldn't need it anymore but I left it in the project ;-)
+
The g_ANIM_DEFAULT is a fallback animation in case an animation doesn't exist.
  
We assign the starting image and the ending image in the spritesheet for each animations. The animations names were declared in the '''"main.lua"''' file (this is where you would add any extra animations).
+
The animations names were declared in the '''"main.lua"''' file (this is where you would add any extra animations).
  
 
  '''SELF PROMOTION: I made an app to count images in a spritesheet: https://mokatunprod.itch.io/spritesheet-maker-viewer'''
 
  '''SELF PROMOTION: I made an app to count images in a spritesheet: https://mokatunprod.itch.io/spritesheet-maker-viewer'''

Latest revision as of 05:50, 7 September 2025

ePlayer1.lua

This is going to be our first actor. Each actor will be an ECS Entity, let's create a file "ePlayer1.lua" in the "_E" folder.

The arguments of the init function are: the layer the player sprite will be added to, the position as a vector, and the layer for any fancy graphics effects we may add to the player.

The code:

EPlayer1 = Core.class()

function EPlayer1:init(xspritelayer, xpos, xbgfxlayer)
	-- ids
	self.isplayer1 = true
	self.doanimate = true -- to save some cpu
	self.ispersistent = true -- keep sprite visible when dead
	-- sprite layers
	self.spritelayer = xspritelayer
	self.bgfxlayer = xbgfxlayer
	-- params
	self.pos = xpos
	self.sx = 1 -- 0.96, 0.8, 1.05, 1.1, 1.2
	self.sy = self.sx
	self.flip = 1
	self.totallives = 3
	self.totalhealth = 3
	if g_difficulty == 0 then -- easy
		self.totallives *= 2
		self.totalhealth *= 2
	end
	self.currlives = self.totallives
	self.currhealth = self.totalhealth
	-- recovery
	self.washurt = 0
	self.wasbadlyhurt = 0
	self.recovertimer = 20 -- 30
	self.recoverbadtimer = 60 -- 90
	if g_difficulty == 0 then -- easy
		self.recovertimer *= 2
		self.recoverbadtimer *= 2
	elseif g_difficulty == 2 then -- hard
		self.recovertimer *= 0.5
		self.recoverbadtimer *= 0.5
	end
	self.hitfx = Bitmap.new(Texture.new("gfx/fxs/2.png"))
	self.hitfx:setAnchorPoint(0.5, 0.5)
	-- COMPONENTS
	-- ANIMATION
	local anims = {} -- table to hold actor animations
	local animsimgs = {} -- table to hold actor animations images
	-- CAnimation:init(xanimspeed)
	local framerate = 1/10 -- 1/14, 1/18
	self.animation = CAnimation.new(framerate)
	-- CAnimation:cutSpritesheet(xspritesheetpath, xcols, xrows, xanimsimgs, xoffx, xoffy, sx, sy)
	local texpath = "gfx/player1/Matt_0001.png"
	self.animation:cutSpritesheet(texpath, 9, 6, animsimgs, 0, 1, self.sx, self.sy)
	-- 1st set of animations: CAnimation:createAnim(xanims, xanimname, xanimsimgs, xtable, xstart, xfinish)
	self.animation:createAnim(anims, g_ANIM_DEFAULT, animsimgs, nil, 1, 15)
	self.animation:createAnim(anims, g_ANIM_IDLE_R, animsimgs, nil, 1, 15) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_RUN_R, animsimgs, nil, 16, 22) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_JUMPUP_R, animsimgs, nil, 23, 23) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_JUMPDOWN_R, animsimgs, nil, 23, 25) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_LADDER_IDLE_R, animsimgs, nil, 1, 15) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_LADDER_UP_R, animsimgs, nil, 16, 22) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_LADDER_DOWN_R, animsimgs, nil, 16, 22) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_WALL_IDLE_R, animsimgs, nil, 33, 33) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_WALL_UP_R, animsimgs, nil, 26, 37) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_WALL_DOWN_R, animsimgs, nil, 43, 54) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_HURT_R, animsimgs, { 38, 39, 38, 39, 1, }) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_LOSE1_R, animsimgs, nil, 38, 42) -- fluid is best
	self.animation:createAnim(anims, g_ANIM_STANDUP_R, animsimgs, { 42, 41, 40, 39, 1, }) -- fluid is best
	self.animation.anims = anims
	self.sprite = self.animation.sprite
	self.animation.sprite = nil -- free some memory
	self.w, self.h = self.sprite:getWidth(), self.sprite:getHeight() -- with applied scale
	print("player1 size (scaled): ", self.w, self.h)
	-- BODY: CBody:init(xmass, xspeed, xupspeed, xextra)
	self.body = CBody.new(1, 18*8, 68*8, true) -- (1, 22*8, 70*8, true)
	-- COLLISION BOX: CCollisionBox:init(xcollwidth, xcollheight)
	local collw, collh = (self.w*0.3)//1, (self.h*0.75)//1 -- must be round numbers for cbump physics!
	self.collbox = CCollisionBox.new(collw, collh)
	-- shield
	self.shield = {}
--	self.shield.sprite = Bitmap.new(Texture.new("gfx/fxs/Husky_0001.png"))
	self.shield.sprite = Pixel.new(0xffaa00, 0.75, 8, collh+4)
	self.shield.sprite.sx = 1
	self.shield.sprite.sy = self.shield.sprite.sx
	self.shield.sprite:setScale(self.shield.sprite.sx, self.shield.sprite.sy)
	self.shield.sprite:setAlpha(0.8)
	self.shield.sprite:setAnchorPoint(0.5, 0.5)
	self.spritelayer:addChild(self.shield.sprite)
	self.shield.offset = vector(collw, (collh-4)*0.5) -- (5*8, -1*8)
	self.shield.timer = 3*8 -- 2*8
	self.shield.currtimer = self.shield.timer
	self.shield.damage = 0.5
end

Code comments

Let's break it down!

ids

Ids are used as filters by the ECS systems. Based on an Entity id, a System will process it or not. Here we have an id to tell this is the player1 Entity, an id telling this sprite is animated, ...

All the player1 variables below can be used as ids as well, should we want to narrow down a System filter

variables: the basics

The systems will need to know a bunch of information about the Entity it is processing:

  • the sprite layers the actor lives in
  • the actor position and scale
  • flip to indicate the direction the actor is facing
  • the number of lives, health, jumps, ...

recovery

When the actor is hit, we give it some time to recover. During this time the actor is invincible.

fx

self.hitfx is one of those sprite we add to the fxlayer when the player1 is hit by another actor.

COMPONENTS

The beauty of ECS is its modularity. Let's add our player1 some components. A Component is an ability you add to an Entity. Entities can and will share the same components.

ANIMATION

The first component we add is the Animation Component. Create a file called "cAnimation.lua" in the "_C" folder and add the following code:

CAnimation = Core.class()

function CAnimation:init(xanimspeed)
	-- animation
	self.curranim = g_ANIM_DEFAULT
	self.frame = 0
	self.animspeed = xanimspeed
	self.animtimer = self.animspeed
end

function CAnimation:cutSpritesheet(xspritesheetpath, xcols, xrows, xanimsimgs, xoffx, xoffy, sx, sy)
	-- retrieve all anims in texture
	local myanimstex = Texture.new(xspritesheetpath)
	local cellw = myanimstex:getWidth()/xcols
	local cellh = myanimstex:getHeight()/xrows
	for r = 1, xrows do
		for c = 1, xcols do
			local myanimstexregion = TextureRegion.new(
				myanimstex, (c-1)*cellw, (r-1)*cellh, cellw, cellh
			)
			xanimsimgs[#xanimsimgs + 1] = myanimstexregion
		end
	end
	-- the bitmap
	self.bmp = Bitmap.new(xanimsimgs[1]) -- starting bmp texture
	self.bmp:setScale(sx, sy) -- scale!
	self.bmp:setAnchorPoint(0.5, 0.5) -- we will flip the bitmap
	-- set position inside sprite
	self.bmp:setPosition(xoffx*sx, xoffy*sy) -- work best with image centered spritesheets
	-- our final sprite
	self.sprite = Sprite.new()
	self.sprite:addChild(self.bmp)
	self.bmp:setColorTransform(8*32/255, 8*32/255, 8*32/255, 9*32/255)
end

function CAnimation:createAnim(xanims, xanimname, xanimsimgs, xtable, xstart, xfinish)
	xanims[xanimname] = {}
	if xtable and #xtable > 0 then
		for i = 1, #xtable do
			xanims[xanimname][#xanims[xanimname]+1] = xanimsimgs[xtable[i]]
		end
	else
		for i = xstart, xfinish do
			xanims[xanimname][#xanims[xanimname]+1] = xanimsimgs[i]
		end
	end
end

In the init function we simply indicate the animation speed, then we initialize the basics variables.

Then the Animation Component cuts the spritesheet into single images.

Finally the Animation Component creates the animations table with the animation name and images.

animations

Back to the "ePlayer1.lua" code we create the animations.

To add images to an animation you can pass a table with the spritesheet images cell number:

	self.animation:createAnim(anims, g_ANIM_STANDUP_R, animsimgs, { 42, 41, 40, 39, 1, }, nil)

Or you assign the starting image and the ending image in the spritesheets:

	self.animation:createAnim(anims, g_ANIM_DEFAULT, animsimgs, nil, 1, 15)
	self.animation:createAnim(anims, g_ANIM_IDLE_R, animsimgs, nil, 1, 15)

The g_ANIM_DEFAULT is a fallback animation in case an animation doesn't exist.

The animations names were declared in the "main.lua" file (this is where you would add any extra animations).

SELF PROMOTION: I made an app to count images in a spritesheet: https://mokatunprod.itch.io/spritesheet-maker-viewer

Next?

To shorten the length of this part, let's see the other components in the next chapter.


Prev.: Tuto tiny-ecs 2d platformer Part 4 LevelX
Next: Tuto tiny-ecs 2d platformer Part 6 ECS Components


Tutorial - tiny-ecs 2d platformer