Implement character animations and dynamic sprite character loading.

This commit is contained in:
sigonasr2 2025-08-26 05:57:08 -05:00
parent b145bb07d0
commit 39b88ce06e
55 changed files with 124 additions and 16 deletions

1
.gitignore vendored
View File

@ -444,4 +444,3 @@ FodyWeavers.xsd
/CMakeCache.txt
/assets/gfx/Cute RPG
/bin/assets/gfx/Cute RPG
/bin

BIN
bin/Shep.data Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

1
bin/Shep.html Normal file

File diff suppressed because one or more lines are too long

1
bin/Shep.js Normal file

File diff suppressed because one or more lines are too long

BIN
bin/Shep.wasm Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
- Credits to PixyMoon for many pixel art graphics, characters, and world tiles that are used in the game
Cute RPG Graphics

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -38,9 +38,19 @@ All rights reserved.
#pragma once
enum class AnimationState{
STAND,
STAND_N,
STAND_E,
STAND_S,
STAND_W,
WALK_N,
WALK_E,
WALK_S,
WALK_W,
};
enum class SkinTone{
TONE_1,
TONE_2,
TONE_3,
TONE_4,
};

View File

@ -65,6 +65,34 @@ const hw3d::mesh&GameObject::GetMesh()const{
}
}
const std::vector<std::array<float,2>>GameObject::GetUVs()const{
if(anim){
switch(GetMeshType()){
case MeshType::FLOOR:
case MeshType::OBJ:{
std::cout<<"WARNING! Unimplemented animation code reached!"<<std::endl;
throw;
}break;
case MeshType::SPRITE:{
geom2d::rect<float>uv{animState.GetFrame(*anim).GetSourceUV()};
return std::vector<std::array<float,2>>{{
{{uv.pos.x,uv.pos.y+uv.size.y}},
{{uv.pos.x+uv.size.x,uv.pos.y+uv.size.y}},
{{uv.pos.x+uv.size.x,uv.pos.y}},
{{uv.pos.x,uv.pos.y+uv.size.y}},
{{uv.pos.x+uv.size.x,uv.pos.y}},
{{uv.pos.x,uv.pos.y}},
}};
}break;
default:{
std::cout<<"WARNING! Mesh Type is not any of the expected values! THIS SHOULD NOT BE HAPPENING!"<<std::endl;
throw;
}
}
}
else return GetMesh().uv;
}
const vf3d&GameObject::GetPos()const{
return pos;
}
@ -74,7 +102,8 @@ const vf3d&GameObject::GetScale()const{
}
const Renderable&GameObject::GetSprite()const{
return ShepGame::Game().GetSpr(spriteMeshName);
if(anim)return *animState.GetFrame(*anim).GetSourceImage();
else return ShepGame::Game().GetSpr(spriteMeshName);
}
void GameObject::SetAutoScale(const vf2d&unitDivision){
@ -85,15 +114,47 @@ void GameObject::SetAutoScale(const vf2d&unitDivision){
}
void GameObject::Update(const float&fElapsedTime){
if(anim)ShepGame::Game().animation.UpdateState(*anim,fElapsedTime);
if(anim)animState.UpdateState(*anim,fElapsedTime);
auto&game{ShepGame::Game()};
switch(id){
case ObjectID::PLAYER:{
if(game.GetKey(Key::W).bHeld){pos.z-=fElapsedTime*GameSettings::playerSpd;}
if(game.GetKey(Key::A).bHeld){pos.x-=fElapsedTime*GameSettings::playerSpd;}
if(game.GetKey(Key::S).bHeld){pos.z+=fElapsedTime*GameSettings::playerSpd;}
if(game.GetKey(Key::D).bHeld){pos.x+=fElapsedTime*GameSettings::playerSpd;}
if(game.GetKey(Key::W).bHeld){
pos.z-=fElapsedTime*GameSettings::playerSpd;
if(anim)animState.ChangeState(*anim,AnimationState::WALK_N);
}
if(game.GetKey(Key::A).bHeld){
pos.x-=fElapsedTime*GameSettings::playerSpd;
if(anim)animState.ChangeState(*anim,AnimationState::WALK_W);
}
if(game.GetKey(Key::S).bHeld){
pos.z+=fElapsedTime*GameSettings::playerSpd;
if(anim)animState.ChangeState(*anim,AnimationState::WALK_S);
}
if(game.GetKey(Key::D).bHeld){
pos.x+=fElapsedTime*GameSettings::playerSpd;
if(anim)animState.ChangeState(*anim,AnimationState::WALK_E);
}
if(game.GetKey(Key::W).bReleased){
if(anim)animState.ChangeState(*anim,AnimationState::STAND_N);
}
if(game.GetKey(Key::A).bReleased){
if(anim)animState.ChangeState(*anim,AnimationState::STAND_W);
}
if(game.GetKey(Key::S).bReleased){
if(anim)animState.ChangeState(*anim,AnimationState::STAND_S);
}
if(game.GetKey(Key::D).bReleased){
if(anim)animState.ChangeState(*anim,AnimationState::STAND_E);
}
}break;
}
}
void GameObject::ApplyCharacterAnimation(const uint8_t animInd,const SkinTone tone){
std::unordered_map<AnimationState,Animate2D::FrameSequence>&animations{ShepGame::Game().GetCharacterAnimation(animInd,tone)};
for(AnimationState&state:std::vector<AnimationState>{AnimationState::STAND_S,AnimationState::STAND_W,AnimationState::STAND_E,AnimationState::STAND_N,AnimationState::WALK_S,AnimationState::WALK_W,AnimationState::WALK_E,AnimationState::WALK_N}){
animState.AddState(state,animations.at(state));
}
anim=Animate2D::AnimationState{};
}

View File

@ -52,16 +52,18 @@ public:
void Update(const float&fElapsedTime);
const MeshType GetMeshType()const;
const hw3d::mesh&GetMesh()const;
const std::vector<std::array<float,2>>GetUVs()const;
const vf3d&GetPos()const;
const vf3d&GetScale()const;
const Renderable&GetSprite()const;
void SetAutoScale(const vf2d&unitDivision); //Takes the texture width/height and divides it by unitDivion then scales the Game Object accordingly. Useful for scaling tilemaps.
void ApplyCharacterAnimation(const uint8_t animInd,const SkinTone tone);
private:
vf3d pos;
vf3d scale;
ObjectID id;
std::string spriteMeshName;
MeshType type;
AnimationState state;
std::optional<Animate2D::AnimationState>anim;
Animate2D::Animation<AnimationState>animState;
};

View File

@ -41,12 +41,13 @@ void ShepGame::LoadObj(const std::string&filename){
else assets[texFilename].Load("assets/models/"+texFilename);
}
void ShepGame::LoadSprite(const std::string&filename){
const Renderable&ShepGame::LoadSprite(const std::string&filename){
if(assets.count(filename)){
std::cout<<"WARNING! Duplicate sprite filename detected: "<<filename<<std::endl;
throw;
}
assets[filename].Load("assets/gfx/"+filename);
return assets.at(filename);
}
GameObject&ShepGame::AddGameObject(const vf3d&pos,const vf3d&scale,const GameObject::ObjectID&id,const std::string&spriteMeshName,const MeshType&type){
@ -96,12 +97,12 @@ bool ShepGame::OnUserCreate(){
InitializeSpriteMesh();
InitializeFloorSpriteMesh();
LoadAnimations();
LoadMap("Town1.tmx");
AddGameObject({0,0,0},{1,2,1},GameObject::ObjectID::PLAYER,"nico-Trapper_512.png",MeshType::SPRITE);
auto&town{AddGameObject({0,0,0},{1,1,1},GameObject::ObjectID::DEFAULT,"Town1.png",MeshType::FLOOR)};
town.SetAutoScale({16,16});
auto&player{AddGameObject({0,0,0},{1,2,1},GameObject::ObjectID::PLAYER,"nico-trapper.png",MeshType::SPRITE)};
player.ApplyCharacterAnimation(29,SkinTone::TONE_2);
AddLight({{4,0,10},WHITE});
@ -144,6 +145,33 @@ void ShepGame::LoadMap(const std::string&filename){
}
}
}
auto&town{AddGameObject({0,0,0},{1,1,1},GameObject::ObjectID::DEFAULT,"../maps/"+filename.substr(0,filename.length()-3)+"png",MeshType::FLOOR)};
town.SetAutoScale({16,16});
}
std::unordered_map<AnimationState,Animate2D::FrameSequence>&ShepGame::GetCharacterAnimation(const uint8_t animInd,const SkinTone tone){
return characterAnimations.at((animInd-1)*4+int(tone));
}
void ShepGame::LoadAnimations(){
for(int i:std::ranges::iota_view(1,42)){
const Renderable&sheet{LoadSprite(std::format("Character_{:03}.png",i))};
for(int j:std::ranges::iota_view(0,4)){
std::unordered_map<AnimationState,Animate2D::FrameSequence>animation;
(*animation.insert({AnimationState::STAND_N,Animate2D::FrameSequence{0.2f,Animate2D::Style::Repeat}}).first).second.AddFrame(Animate2D::Frame{&sheet,{{24+j*96,24*3},{24,24}}});
(*animation.insert({AnimationState::STAND_E,Animate2D::FrameSequence{0.2f,Animate2D::Style::Repeat}}).first).second.AddFrame(Animate2D::Frame{&sheet,{{24+j*96,24*2},{24,24}}});
(*animation.insert({AnimationState::STAND_S,Animate2D::FrameSequence{0.2f,Animate2D::Style::Repeat}}).first).second.AddFrame(Animate2D::Frame{&sheet,{{24+j*96,24*0},{24,24}}});
(*animation.insert({AnimationState::STAND_W,Animate2D::FrameSequence{0.2f,Animate2D::Style::Repeat}}).first).second.AddFrame(Animate2D::Frame{&sheet,{{24+j*96,24*1},{24,24}}});
for(uint8_t row{};AnimationState&state:std::vector<AnimationState>{AnimationState::WALK_S,AnimationState::WALK_W,AnimationState::WALK_E,AnimationState::WALK_N}){
animation.insert({state,Animate2D::FrameSequence{0.2f,Animate2D::Style::Repeat}});
for(int k:std::ranges::iota_view(0,4)){
animation[state].AddFrame(Animate2D::Frame{&sheet,{{24*k+j*96,24*row},{24,24}}});
}
row++;
}
characterAnimations.emplace_back(std::move(animation));
}
}
}
bool ShepGame::OnUserUpdate(float fElapsedTime){
@ -211,7 +239,7 @@ bool ShepGame::OnUserUpdate(float fElapsedTime){
matWorld.translate(vf3d{-0.5f,0,-0.5f}+pos+obj.GetPos());
mf4d scale;
scale.scale(obj.GetScale());
HW3D_DrawObject((matView * matWorld * scale).m, obj.GetSprite().Decal(), mesh.layout, mesh.pos, mesh.uv, mesh.col);
HW3D_DrawObject((matView * matWorld * scale).m, obj.GetSprite().Decal(), mesh.layout, mesh.pos, obj.GetUVs(), mesh.col);
};
for(GameObject&obj:objects){

View File

@ -57,7 +57,7 @@ public:
const hw3d::mesh&GetSpriteMesh()const;
static ShepGame&Game();
void LoadMap(const std::string&filename);
Animate2D::Animation<AnimationState>animation;
std::unordered_map<AnimationState,Animate2D::FrameSequence>&GetCharacterAnimation(const uint8_t animInd,const SkinTone tone); //Provide the EXACT index of the character you want from the character images in gfx. NOTE: They are 1-indexed! Use 1 for the first one etc.
private:
static ShepGame*game;
@ -77,7 +77,10 @@ private:
void InitializeSpriteMesh();
void InitializeFloorSpriteMesh();
void LoadObj(const std::string&filename); //filename is the base filename inside the assets/models directory. NOTE: Textures loaded from this function are also based from the assets/models directory and match the model name Ex. building1.obj expects building1.png.
void LoadSprite(const std::string&filename); //filename is the base filename inside the assets/gfx directory
const Renderable&LoadSprite(const std::string&filename); //filename is the base filename inside the assets/gfx directory
void LoadAnimations();
std::vector<std::unordered_map<AnimationState,Animate2D::FrameSequence>>characterAnimations;
virtual bool OnUserCreate()override;