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

From GiderosMobile
(Created page with "__TOC__ == the levelX.lua file == This is where all the fun begins! The LevelX scene holds the game loop and controls the flow of each levels. When the scene loads, it cons...")
 
m
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
__TOC__
 
__TOC__
  
== the levelX.lua file ==
+
== ePlayer1.lua ==
This is where all the fun begins!
+
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 LevelX scene holds the game loop and controls the flow of each levels.
+
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.
  
When the scene loads, it constructs the level which is organised into layers, more on that in the code comments below ;-)
+
The code:
 +
<syntaxhighlight lang="lua">
 +
EPlayer1 = Core.class()
  
The code is not that long given it takes care of all levels of the game: level 1, 2 and 3. To make it easy to follow and debug, I use '''FIGlet''' (https://en.wikipedia.org/wiki/FIGlet).
+
function EPlayer1:init(xspritelayer, xpos, xbgfxlayer)
 
+
-- ids
'''I use this one https://sourceforge.net/projects/figletgenerator/'''
+
self.isplayer1 = true
 
+
self.doanimate = true -- to save some cpu
The '''LevelX''' code:
+
-- sprite layers
<syntaxhighlight lang="lua">
+
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
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== levelX.lua Code comments ==
+
== Code comments ==
 
Let's break it down!
 
Let's break it down!
  
The ''--!strict'' thing is some Luau stuff I was messing around with, you can find more information here: https://devforum.roblox.com/t/luau-type-checking-beta/435382#strict-mode-5.
+
=== 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.
I declare 2 variables which are local to the Class because I didn't want to use ''self'' on them. The ''random'' variable caches the ''math.random'' function for speed. I am also experimenting here with Luau type annotations https://devforum.roblox.com/t/luau-type-checking-beta/435382#new-syntax-6.
 
 
 
<syntaxhighlight lang="lua">
 
local random = math.random
 
local ispaused : boolean = false
 
</syntaxhighlight>
 
 
 
=== LevelX:init() ===
 
I commented out the "move cursor" code because it was more annoying than anything else! You can use it if you want.
 
 
 
==== plugins ====
 
Here we initialize our plugins. tiny-ecs is declared as a Class variable (''self.tiny'') because we use it outside the ''init'' function. Bump can be local as we only use it in the ''init'' function.
 
 
 
It is worth noting that ''tiny.tworld'' is attached to ''self.tiny'' variable. This makes it much easier to access through the rest of the code in our project.
 
 
 
==== layers ====
 
This one is easy :-)
 
  
We create several layers which will be laid out on top of each other. The background layer will have all the graphics for the background, etc...
+
'''All the player1 variables below can be used as ids as well, should we want to narrow down a System filter'''
  
There is a ''player1inputlayer'' which will capture the user input to control the player, it won't hold any graphics.
+
=== 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, ...
  
==== levels ====
+
=== recovery ===
Time to build our levels. The sprite list is a table of all the actors we can interact with (the player, the nmes, collectibles, ...). We put them in a list so we can use that list in the systems we will create (sAI, sAnimation, sCollectible, sCollision, ...).
+
When the actor is hit, we give it some time to recover. During this time the actor is invincible.
  
''tiny.numberofnmes'' and ''tiny.numberofdestructibleobjects'' are variables we can tune to add a certain amount of enemies and destructible objects in each level.
+
=== my mistake! ===
 +
I added a ''self.ispaused'' variable which is completly useless, you can safely delete it :-)
  
The ''mapdef'' is a table which has the map definition (dimensions), so we can pass it to functions that will require the size of the map for calculations.
+
''self.hitfx'' is one of those fx we add to the fxlayer when the player1 successfully hits another actor.
  
We use a camera in our game and ''camfollowoffsety'' will offset the camera following the player. It is easier to change it being a variable.
+
== 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.
  
The ''buildLevel'' function is where we build the current level the player is playing. We pass it some variables so it can do its thing. Some code comments on that function below.
+
=== 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:
Then we set the variables for each level: the camera offset and the number of destructible objects. The destructible objects spawn collectible when they are destroyed (extended life, jumps).
 
 
 
We randomly place the destructible objects throughout the level using the map definition. We place them between 25% and 90% of the map length and a little bit above the bottom of the map.
 
 
 
Each destructible objects are entities and we add them to both the '''tiny-ECS''' world and the '''Bump''' world. We will create entities in the coming parts of the tutorial.
 
 
 
==== player1 ====
 
The player is an ECS entity we will create in the next chapters. 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.
 
 
 
After the entity is created, we add it to both the '''tiny-ECS''' world and the '''Bump''' world.
 
 
 
==== hud ====
 
I added a simple head up display to the game, so we can see the player current health, number of lives and number of attacking jumps available.
 
 
 
The same way we attached ''tiny.tworld'' to the ''self.tiny'' variable, we attach some more variables to it. This makes it easier to access those variables.
 
 
 
==== the camera ====
 
We use a slightly modified version of '''MultiPain''''s (aka rrraptor on Gideros forum) '''GCam''' Class for our camera.
 
 
 
Please grab it here '''[[Media:gcam_beu.lua]]''' '''(tip: right click and save link as)''' and put it in the "'''classes'''" folder.
 
 
 
We pass the ''mainlayer'' as the content, then using the map definition we set the camera bounds. We also set the soft and the dead size parameters and we tell it to follow the player.
 
 
 
'''if you are curious I added an extra updateXOnly function ;-)'''
 
 
 
==== order ====
 
This is the order of the layers, from bottom to top. We first add the background layer and the other layers on top of it.
 
 
 
==== systems ====
 
Once all entities are done, we add the ECS systems. We will see those ECS systems in the coming parts of the tutorial.
 
 
 
==== let's go ====
 
Finally we are ready to run the game loop!
 
 
 
We also listen to some key events to pause the game, go fullscreen, ...
 
 
 
== LevelX:buildLevel ==
 
Let's have a look at how we construct our levels.
 
 
 
=== background and foreground ===
 
We draw the background and foreground for each level using the ''DrawLevelsTiled'' Class. Each level will have its own graphics. A typical level is 3*1024 pixels wide. Using a power of two size for the graphics, we anticipate any kind of optimisations we may need, plus adding the possibility to port the game to mobile!
 
 
 
The ''DrawLevelsTiled'' Class already tries to bring in some optimisations! You can create a file called "drawlevelstiled.lua" and put it in the '''gfx\levels\''' folder. The code:
 
 
<syntaxhighlight lang="lua">
 
<syntaxhighlight lang="lua">
DrawLevelsTiled = Core.class(Sprite)
+
CAnimation = Core.class()
  
function DrawLevelsTiled:init(xlayer, xtexpaths, xposy)
+
function CAnimation:init(xspritesheetpath, xcols, xrows, xanimspeed, xoffx, xoffy, sx, sy)
-- tilemaps textures
+
-- animation
local textures = {}
+
self.curranim = g_ANIM_DEFAULT
for i = 1, #xtexpaths do
+
self.frame = 0
-- textures[i] = Texture.new(xtexpaths[i])
+
self.animspeed = xanimspeed
-- textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.YA8}) -- best win32 perfs but b&w!
+
self.animtimer = self.animspeed
textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA4444}) -- better win32 perfs but !
+
-- retrieve all anims in texture
-- textures[i] = Texture.new(xtexpaths[i], false, { format=TextureBase.RGBA5551}) -- better win32 perfs but !
+
local myanimstex = Texture.new(xspritesheetpath)
end
+
local cellw = myanimstex:getWidth() / xcols
-- map size
+
local cellh = myanimstex:getHeight() / xrows
local tilesizetarget = 64
+
self.myanimsimgs = {}
local tilesetcols, tilesetrows = textures[1]:getWidth()/tilesizetarget, textures[1]:getHeight()/tilesizetarget
+
local myanimstexregion
-- create the tilemaps
+
for r = 1, xrows do
local function createTilemap(xtex)
+
for c = 1, xcols do
local tm = TileMap.new(
+
myanimstexregion = TextureRegion.new(myanimstex, (c - 1) * cellw, (r - 1) * cellh, cellw, cellh)
tilesetcols, tilesetrows, -- map size in tiles
+
self.myanimsimgs[#self.myanimsimgs + 1] = myanimstexregion
xtex, -- tileset texture
 
tilesizetarget, tilesizetarget -- tile size in pixel
 
)
 
-- build the map
 
for i=1,tilesetcols do
 
for j=1,tilesetrows do
 
tm:setTile(i, j, i, j)
 
end
 
 
end
 
end
return tm
 
 
end
 
end
-- the maps
+
-- anims table ("walk", "jump", "shoot", ...)
for i = 1, #textures do
+
self.anims = {}
local map = createTilemap(textures[i])
+
-- the bitmap
map:setPosition(map:getWidth()*(i-1), xposy)
+
self.bmp = Bitmap.new(self.myanimsimgs[1]) -- starting bmp texture
-- self:addChild(map)
+
self.bmp:setScale(sx, sy) -- scale!
xlayer:addChild(map)
+
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
-- params
 
-- self.mapwidth = self:getWidth()
 
-- self.mapheight = self:getHeight()
 
self.mapwidth = xlayer:getWidth()
 
self.mapheight = xlayer:getHeight()
 
-- clean
 
textures = {}
 
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
As the name tries to suggest (DrawLevelsTiled), we take the paths to the graphics and create tiles as if we were drawing using a tilemap. The ''init'' function parameters are: the layer we add the sprites to, the paths to the textures as a table and an y offset for flexibility.
+
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 first step is to create the textures and put them in a table called ''textures''. When we iterate through the ''xtexpaths'' table, I experimented with some texture formats to see if I could gain some speed.
+
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.
  
Once the textures are created and stored in a table, we can create a '''[[TileMap]]''' of 64*64px tiles.
+
==== animations ====
 
+
Back to the '''"ePlayer1.lua"''' code we create its animations.
Finally we return the size of the map and we clean the ''textures'' table just in case.
+
<syntaxhighlight lang="lua">
 
+
self.animation:createAnim(g_ANIM_DEFAULT, 1, 15)
Back to ''LevelX'' Class, now that our background and foreground layers are drawn, we set the map definition. The map definition sets the boundaries of the map where the actors can freely roam.
+
self.animation:createAnim(g_ANIM_IDLE_R, 1, 15) -- fluid is best
 
+
self.animation:createAnim(g_ANIM_WALK_R, 16, 26) -- fluid is best
=== the enemies ===
+
self.animation:createAnim(g_ANIM_JUMP1_R, 74, 76) -- fluid is best
Each level will have different kind of enemies. The first level should be easy with few enemies you can easily kill. The other levels will be increasingly difficult with more enemies a little bit harder to defeat.
+
...
 
+
</syntaxhighlight>
So here, depending on the level we create a bunch of enemies. Enemies are ECS entities we will create in the following chapters. The game has four types of enemies of varying strength. We add them to both the '''tiny-ECS''' world and the '''Bump''' world.
 
 
 
=== extra gfx ===
 
Depending on the level, we may have extra sprites we want to add to the level.
 
 
 
We finish the function with some house cleaning.
 
 
 
== LevelX:onEnterFrame '''THE GAME LOOP''' ==
 
Before the game loop, I declare three local variables: ''leveltimer'' is the time it takes to transition to the next level, ''endleveltimer'' is the timer itself, ''extragfxx'' is to move the extra sprite we may have on the x axis.
 
 
 
When we defeat all enemies, the timer decreases. When it reaches 0, the current level number is increased and we load it.
 
 
 
When the player is jumping we update the '''camera''' only on the x axis, otherwise we update the camera on both x and y axis.
 
  
We update the '''tiny-ecs world''', so it can run all the systems (animations, movements, AI, ...). Some systems will be called only once, others every frame per second.
+
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 ;-)
  
== LevelX:myKeysPressed ==
+
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 last function of the LevelX Class is ''myKeysPressed''.
 
  
Here we simply listen to some '''KEY_DOWN Events''', that is keys pressed on the keyboard. The key ''ESC'' will go back to the menu, the letter ''P'' will pause the game and when you press ''ALT+ENTER'' you switch the game to fullscreen.
+
'''SELF PROMOTION: I made an app to count images in a spritesheet: https://mokatunprod.itch.io/spritesheet-maker-viewer'''
  
 
== Next? ==
 
== Next? ==
That was quite a lot of work but we coded the heart of our game and we are already nearly done!!
+
To shorten the length of this part, let's see the other components in the next chapter.
 
 
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 3 transitions menu options]]</br>
+
Prev.: [[Tuto tiny-ecs beatemup Part 4 LevelX]]</br>
'''Next: [[Tuto tiny-ecs beatemup Part 5 ePlayer1]]'''
+
'''Next: [[Tuto tiny-ecs beatemup Part 6 ECS Components]]'''
  
  
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
'''[[Tutorial - tiny-ecs beatemup]]'''
 
{{GIDEROS IMPORTANT LINKS}}
 
{{GIDEROS IMPORTANT LINKS}}

Latest revision as of 02:22, 20 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 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
	-- 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 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
  • 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 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.

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

Next?

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


Prev.: Tuto tiny-ecs beatemup Part 4 LevelX
Next: Tuto tiny-ecs beatemup Part 6 ECS Components


Tutorial - tiny-ecs beatemup