272 lines
9.2 KiB
C
272 lines
9.2 KiB
C
#include "Player.h"
|
|
#include "debug.h"
|
|
#include "dictionary.h"
|
|
#include "input_axis.h"
|
|
#include "physics_world.h"
|
|
|
|
#include "PlayerStates.h"
|
|
#include "Layers.h"
|
|
#include "program.h"
|
|
#include "variant.h"
|
|
|
|
const Vector PLAYER_SPEED = { 1.0f, 0.50f };
|
|
static const float PLAYER_INPUT_RATE = 1.f/15.f;
|
|
|
|
START_REFLECT(Player)
|
|
REFLECT_TYPECLASS(Player, Drop)
|
|
REFLECT_TYPECLASS(Player, PhysicsEntity)
|
|
REFLECT_TYPECLASS(Player, BehaviourEntity)
|
|
REFLECT_TYPECLASS(Player, Transformable)
|
|
END_REFLECT(Player)
|
|
|
|
impl_Drop_for(Player,
|
|
DestroyPlayer
|
|
)
|
|
|
|
impl_BehaviourEntity_for(Player,
|
|
PlayerStart,
|
|
PlayerUpdate,
|
|
PlayerDraw,
|
|
PlayerGetDepth
|
|
)
|
|
|
|
impl_Transformable_for(Player,
|
|
PlayerGetTransform
|
|
)
|
|
|
|
impl_PhysicsEntity_for(Player,
|
|
PlayerGetRigidBody,
|
|
PlayerOnCollision,
|
|
PlayerOnOverlap
|
|
)
|
|
|
|
static inline
|
|
void Internal_PlayerInitInput(Player* self) {
|
|
playerinput_add(self->playerInput,
|
|
CompositeAxis1D_as_InputAxis(compositeaxis1d_from_keys(SDL_SCANCODE_A, SDL_SCANCODE_D)),
|
|
(InputDelegateFn)PlayerHorizontalInput);
|
|
playerinput_add(self->playerInput,
|
|
CompositeAxis1D_as_InputAxis(compositeaxis1d_from_keys(SDL_SCANCODE_S, SDL_SCANCODE_W)),
|
|
(InputDelegateFn)PlayerVerticalInput);
|
|
playerinput_add(self->playerInput,
|
|
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_SPACE)),
|
|
(InputDelegateFn)PlayerJumpInput);
|
|
playerinput_add(self->playerInput,
|
|
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_J)),
|
|
(InputDelegateFn)PlayerLightAttackInput);
|
|
playerinput_add(self->playerInput,
|
|
KeyBind_as_InputAxis(keybind_new(SDL_SCANCODE_K)),
|
|
(InputDelegateFn)PlayerHeavyAttackInput);
|
|
}
|
|
|
|
Player* MakePlayer() {
|
|
Player* self = malloc(sizeof(Player));
|
|
ASSERT_RETURN(self != NULL, NULL, "Failed to allocate space for new Player instance");
|
|
|
|
*self = (Player) {
|
|
.transform = IdentityTransform,
|
|
.height = 0.f,
|
|
|
|
.rigidbody = NULL,
|
|
.physicsCollider = NULL,
|
|
.hitbox = NULL,
|
|
|
|
.verticalVelocity = 0.f,
|
|
|
|
.playerInput = playerinput_new(self, -1),
|
|
.moveInput = ZeroVector,
|
|
.attackInput = 0,
|
|
.animationTriggers = 0,
|
|
|
|
.facing = 1,
|
|
|
|
.sprite = sprite_new_empty(),
|
|
|
|
.idle = NULL,
|
|
.walk = NULL,
|
|
.jump = NULL,
|
|
.jab_a = NULL,
|
|
.jab_b = NULL,
|
|
.kick_a = NULL,
|
|
.slash = NULL,
|
|
.air_heavy = NULL,
|
|
.slide = NULL,
|
|
|
|
.animationStateMachine = NULL,
|
|
|
|
.pushInputTimer = 0.f,
|
|
.inputLog = list_from_type(PlayerInputFrame),
|
|
};
|
|
|
|
self->rigidbody = rigidbody_make(Player_as_PhysicsEntity(self));
|
|
// physics collider used for movement
|
|
self->physicsCollider = collider_new(Player_as_PhysicsEntity(self), shape_new((Vector[]){
|
|
MakeVector(-0.2f, -0.065f),
|
|
MakeVector( 0.2f, -0.065f),
|
|
MakeVector( 0.2f, 0.065f),
|
|
MakeVector(-0.2f, 0.065f)
|
|
}, 4), 0, PHYSICS_LAYER_CHARACTERS, PHYSICS_LAYER_DEFAULT);
|
|
// hitbox is used for combat only
|
|
self->hitbox = collider_new(Player_as_PhysicsEntity(self), shape_new((Vector[]){
|
|
MakeVector(-0.1f, -0.9f),
|
|
MakeVector( 0.1f, -0.9f),
|
|
MakeVector( 0.1f, 0.00f),
|
|
MakeVector(-0.1f, 0.00f)
|
|
}, 4), 1, PHYSICS_LAYER_COMBAT, 0x0); // empty mask and overlap means this does not detect collisions itself
|
|
|
|
sprite_set_origin(self->sprite, MakeVector(0.45f, 0.925f));
|
|
|
|
// load and configure animations
|
|
self->idle = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Idle.png", IVectorFrom(512)), 1.5f, LoopMode_Loop);
|
|
self->walk = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Walk.png", IVectorFrom(512)), 5.f, LoopMode_Loop);
|
|
self->jump = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jumping.png", IVectorFrom(512)), 1.f, LoopMode_Stop);
|
|
self->jab_a = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jab_A.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
|
|
self->jab_b = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Jab_B.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
|
|
self->kick_a = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Kick_A.png", IVectorFrom(512)), 12.f, LoopMode_Stop);
|
|
self->slash = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Slash.png", IVectorFrom(512)), 12.f, LoopMode_Stop);
|
|
self->air_heavy = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Air_Heavy.png", IVectorFrom(512)), 10.f, LoopMode_Stop);
|
|
self->slide = animation_sprite_new(self->sprite, spritesheet_load("assets/Player_Slide.png", IVectorFrom(512)), 1.f, LoopMode_Loop);
|
|
|
|
self->animationStateMachine = state_machine_init(self, PlayerIdle());
|
|
|
|
return self;
|
|
}
|
|
|
|
BehaviourEntity SpawnPlayer(Dictionary* args) {
|
|
Variant arg;
|
|
Player* self = MakePlayer();
|
|
Internal_PlayerInitInput(self);
|
|
if(dictionary_try_get(args, "position", &arg) && arg.type == Variant_Vector)
|
|
self->transform.position = arg.as_vector;
|
|
return Player_as_BehaviourEntity(self);
|
|
}
|
|
|
|
void DestroyPlayer(Player* self) {
|
|
// deregister and free physics components
|
|
physics_world_remove_entity(Player_as_PhysicsEntity(self));
|
|
collider_destroy(self->physicsCollider);
|
|
rigidbody_destroy(self->rigidbody);
|
|
|
|
playerinput_drop(self->playerInput);
|
|
|
|
// erase animations
|
|
animation_sprite_destroy(self->idle);
|
|
animation_sprite_destroy(self->walk);
|
|
animation_sprite_destroy(self->jump);
|
|
animation_sprite_destroy(self->jab_a);
|
|
animation_sprite_destroy(self->jab_b);
|
|
animation_sprite_destroy(self->kick_a);
|
|
animation_sprite_destroy(self->air_heavy);
|
|
animation_sprite_destroy(self->slide);
|
|
sprite_destroy(self->sprite);
|
|
|
|
state_machine_destroy(self->animationStateMachine);
|
|
}
|
|
|
|
void PlayerStart(Player* self) {
|
|
animation_sprite_play_from(self->currentAnimation, 0.0f);
|
|
}
|
|
|
|
static
|
|
void PlayerPushInput(Player* self) {
|
|
// the current input state
|
|
PlayerInputFrame state = {
|
|
.time = game_time(),
|
|
.direction = self->moveInput,
|
|
.light = self->attackInput == 1,
|
|
.heavy = self->attackInput == 2,
|
|
.jump = self->jumpInput,
|
|
.facing = self->facing,
|
|
};
|
|
// push onto the end of the log
|
|
list_add(&self->inputLog, &state);
|
|
// erase oldest input from the log
|
|
if(self->inputLog.len >= 5)
|
|
list_erase(&self->inputLog, 0);
|
|
// log current input state
|
|
LOG_INFO("%f %f, L:%x, H:%x, J:%x",
|
|
state.direction.x, state.direction.y,
|
|
state.light, state.heavy, state.jump);
|
|
}
|
|
|
|
void PlayerUpdate(Player* self, float deltaTime) {
|
|
// update state machine
|
|
state_machine_update(self->animationStateMachine, deltaTime);
|
|
// update gravity
|
|
self->height += self->verticalVelocity * deltaTime;
|
|
if(self->height <= 0.f)
|
|
self->height = 0.f;
|
|
}
|
|
|
|
void PlayerDraw(Player* self) {
|
|
// create a new transform that adjusts the "ground" transform to reflect distance from the ground as well
|
|
Transform trans = self->transform;
|
|
trans.position.y -= self->height;
|
|
animation_sprite_draw(self->currentAnimation, &trans);
|
|
}
|
|
|
|
void PlayerHorizontalInput(Player* self, InputEvent value) {
|
|
self->moveInput.x = value.as_float;
|
|
PlayerPushInput(self);
|
|
}
|
|
|
|
void PlayerVerticalInput(Player* self, InputEvent value) {
|
|
self->moveInput.y = -value.as_float;
|
|
PlayerPushInput(self);
|
|
}
|
|
|
|
void PlayerJumpInput(Player* self, InputEvent value) {
|
|
if(value.as_bool) {
|
|
self->jumpInput = value.as_bool;
|
|
PlayerPushInput(self);
|
|
}
|
|
}
|
|
|
|
void PlayerLightAttackInput(Player* self, InputEvent value) {
|
|
if(value.as_bool) {
|
|
self->attackInput = 1;
|
|
PlayerPushInput(self);
|
|
}
|
|
}
|
|
|
|
void PlayerHeavyAttackInput(Player* self, InputEvent value) {
|
|
if(value.as_bool) {
|
|
self->attackInput = 2;
|
|
PlayerPushInput(self);
|
|
}
|
|
}
|
|
|
|
void PlayerOnCollision(Player* self, Collision collision) {}
|
|
void PlayerOnOverlap(Player* self, Collider* other) {}
|
|
|
|
Transform* PlayerGetTransform(Player* self) {
|
|
return &self->transform;
|
|
}
|
|
|
|
RigidBody* PlayerGetRigidBody(Player* self) {
|
|
return self->rigidbody;
|
|
}
|
|
|
|
PlayerInputFrame* PlayerInputHistory(Player* self) {
|
|
return (PlayerInputFrame*)self->inputLog.data;
|
|
}
|
|
|
|
int PlayerInputIsQuarterCircleForward(Player* self) {
|
|
// at least four inputs have to be logged for a valid quartercircle
|
|
// (three directional, one button, in that order)
|
|
if(self->inputLog.len < 4)
|
|
return 0;
|
|
PlayerInputFrame* history = PlayerInputHistory(self);
|
|
// the most recent input (assumed to be a button input)
|
|
const size_t last = self->inputLog.len-1;
|
|
// the forward direction at the assumed start of the action
|
|
const float forward = history[last-3].facing;
|
|
if(game_time() - history[last-2].time > 0.225f)
|
|
return 0;
|
|
// check if the three inputs before the most recent input are a quartercircle towards the facing direction at last-3 (three before current)
|
|
// current (history[last]) is assumed to be a button input
|
|
return veqf(history[last-3].direction, MakeVector(0.f, 1.f))
|
|
&& veqf(history[last-2].direction, MakeVector(forward, 1.f))
|
|
&& veqf(history[last-1].direction, MakeVector(forward, 0.f));
|
|
}
|