Difference between revisions of "Tuto tiny-ecs beatemup Part 5 ePlayer1"

From GiderosMobile
(wip)
(wip)
Line 185: Line 185:
  
 
=== ids ===
 
=== ids ===
Ids are used as filters by the ECS systems. Based on an Entity id, a System will process it or not. Here is 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 and an id telling this sprite is animated.
  
  '''All the basics variables below can be used as id as well, if we want to narrow down a System filter'''
+
  '''All the player1 variables below can be used as id as well, should we want to narrow down a System filter'''
  
 
=== variables: the basics  ===
 
=== variables: the basics  ===
Line 201: Line 201:
  
 
=== my mistake! ===
 
=== my mistake! ===
I added a ''self.ispaused'' variable which is completly useless here, you can safely delete it :-)
+
I added a ''self.ispaused'' variable which is completly useless, you can safely delete it :-)
  
 
''self.hitfx'' is one of those fx we add to the fxlayer when the player1 successfully hits another actor.
 
''self.hitfx'' is one of those fx we add to the fxlayer when the player1 successfully hits another actor.
  
 
== COMPONENTS ==
 
== 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:
 +
<syntaxhighlight lang="lua">
 +
CAnimation = Core.class()
 +
 +
function CAnimation:init(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
 +
-- animation
 +
self.curranim = g_ANIM_DEFAULT
 +
self.frame = 0
 +
self.animspeed = xanimspeed
 +
self.animtimer = self.animspeed
 +
-- retrieve all anims in texture
 +
local myanimstex = Texture.new(xspritesheetpath)
 +
local cellw = myanimstex:getWidth() / xcols
 +
local cellh = myanimstex:getHeight() / xrows
 +
self.myanimsimgs = {}
 +
local myanimstexregion
 +
for r = 1, xrows do
 +
for c = 1, xcols do
 +
myanimstexregion = TextureRegion.new(myanimstex, (c - 1) * cellw, (r - 1) * cellh, cellw, cellh)
 +
self.myanimsimgs[#self.myanimsimgs + 1] = myanimstexregion
 +
end
 +
end
 +
-- anims table ("walk", "jump", "shoot", ...)
 +
self.anims = {}
 +
-- the bitmap
 +
self.bmp = Bitmap.new(self.myanimsimgs[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)
 +
end
 +
 +
function CAnimation:createAnim(xanimname, xstart, xfinish)
 +
self.anims[xanimname] = {}
 +
for i = xstart, xfinish do
 +
self.anims[xanimname][#self.anims[xanimname]+1] = self.myanimsimgs[i]
 +
end
 +
end
 +
</syntaxhighlight>
 +
 +
The parameters are:
 +
* 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 put 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.
 +
 +
==== animations ====
 +
Back to the '''"ePlayer1.lua"''' code we create its animations.
 +
<syntaxhighlight lang="lua">
 +
self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
 +
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
 +
...
 +
</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 ;-)
 +
 +
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).
 +
 +
'''SELF PROMOTION: I made an app to count images in a spritesheet: https://mokatunprod.itch.io/spritesheet-maker-viewer'''
 +
 +
=== BODY ===
 
XXX
 
XXX
  

Revision as of 23:34, 18 November 2024

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 to 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
	-- sprite layers
	self.spritelayer = xspritelayer
	self.bgfxlayer = xbgfxlayer
	-- params
	self.pos = xpos
	self.positionystart = 0
	self.sx = 1
	self.sy = self.sx
	self.flip = 1
	self.totallives = 3
	self.totalhealth = 10
	self.currjumps = 5
	if g_difficulty == 0 then -- easy
		self.totallives = 5
		self.totalhealth = 20
		self.currjumps = 8
	elseif g_difficulty == 2 then -- hard
		self.currjumps = 3
	end
	self.currlives = self.totallives
	self.currhealth = self.totalhealth
	-- recovery
	self.washurt = 0
	self.wasbadlyhurt = 0
	self.recovertimer = 30
	self.recoverbadtimer = 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.ispaused = false -- 'P' key for pausing the game
	self.hitfx = Bitmap.new(Texture.new("gfx/fx/2.png"))
	self.hitfx:setAnchorPoint(0.5, 0.5)
	-- COMPONENTS
	-- ANIMATION: CAnimation:init(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
	local texpath = "gfx/player1/mh_blue_haired2m_0130.png"
	local framerate = 1/10
	self.animation = CAnimation.new(texpath, 12, 11, framerate, 0, 0, self.sx, self.sy)
	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: ", self.w, self.h)
	-- create basics animations: CAnimation:createAnim(xanimname, xstart, xfinish)
	self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
	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)
	local collw, collh = self.w*0.4, 8*self.sy
	self.collbox = CCollisionBox.new(collw, collh)
	-- HURT BOX
	-- head hurt box w & h
	local hhbw, hhbh = self.w*0.25, self.h*0.3
	self.headhurtbox = {
		isactive=false,
		x=-2*self.sx,
		y=0*self.sy-self.h/2-self.collbox.h*2,
		w=hhbw,
		h=hhbh,
	}
	-- spine hurt box w & h
	local shbw, shbh = self.w*0.35, self.h*0.4
	self.spinehurtbox = {
		isactive=false,
		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

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 and an id telling this sprite is animated.

All the player1 variables below can be used as id 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
  • positionystart the starting position on the y axis before the actor performs a jump
  • 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.

my mistake!

I added a self.ispaused variable which is completly useless, you can safely delete it :-)

self.hitfx is one of those fx we add to the fxlayer when the player1 successfully hits 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(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
	-- animation
	self.curranim = g_ANIM_DEFAULT
	self.frame = 0
	self.animspeed = xanimspeed
	self.animtimer = self.animspeed
	-- retrieve all anims in texture
	local myanimstex = Texture.new(xspritesheetpath)
	local cellw = myanimstex:getWidth() / xcols
	local cellh = myanimstex:getHeight() / xrows
	self.myanimsimgs = {}
	local myanimstexregion
	for r = 1, xrows do
		for c = 1, xcols do
			myanimstexregion = TextureRegion.new(myanimstex, (c - 1) * cellw, (r - 1) * cellh, cellw, cellh)
			self.myanimsimgs[#self.myanimsimgs + 1] = myanimstexregion
		end
	end
	-- anims table ("walk", "jump", "shoot", ...)
	self.anims = {}
	-- the bitmap
	self.bmp = Bitmap.new(self.myanimsimgs[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)
end

function CAnimation:createAnim(xanimname, xstart, xfinish)
	self.anims[xanimname] = {}
	for i = xstart, xfinish do
		self.anims[xanimname][#self.anims[xanimname]+1] = self.myanimsimgs[i]
	end
end

The parameters are:

  • 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 put 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.

animations

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

	self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
	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
	...

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 ;-)

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).

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

BODY

XXX

Next?

That was quite a lot of work but we coded the heart of our game and we are already nearly done!!

All is left to do is add the actors. Actors will be ECS entites, those entities will have components and systems will control them.

In the next part we deal with our player1 entity.


Prev.: [[Tuto tiny-ecs beatemup Part 4 LevelX]]
Next: Tuto tiny-ecs beatemup Part 6 XXX


Tutorial - tiny-ecs beatemup