Character states, aiming and stuff
This commit is contained in:
parent
5b6e4467e9
commit
1079daa49b
@ -97,6 +97,10 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
|
||||
{
|
||||
iss >> anim->tps_;
|
||||
}
|
||||
else if (command == "cyclic")
|
||||
{
|
||||
anim->cyclic_ = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (anim->channels_.empty())
|
||||
@ -120,5 +124,9 @@ std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const s
|
||||
channel.frames = &anim->frame_refs_[i * anim->num_frames_];
|
||||
}
|
||||
|
||||
// calc duration
|
||||
auto frame_range = anim->cyclic_ ? anim->num_frames_ : anim->num_frames_ - 1;
|
||||
anim->duration_ = static_cast<float>(frame_range) / anim->tps_;
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
@ -24,7 +24,8 @@ public:
|
||||
|
||||
size_t GetNumFrames() const { return num_frames_; }
|
||||
float GetTPS() const { return tps_; }
|
||||
float GetDuration() const { return static_cast<float>(num_frames_) / tps_; }
|
||||
float GetDuration() const { return duration_; }
|
||||
bool IsCyclic() const { return cyclic_; }
|
||||
|
||||
size_t GetNumChannels() const { return channels_.size(); }
|
||||
const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
|
||||
@ -32,6 +33,8 @@ public:
|
||||
private:
|
||||
size_t num_frames_ = 0;
|
||||
float tps_ = 24.0f;
|
||||
bool cyclic_ = false;
|
||||
float duration_ = 0.0f;
|
||||
|
||||
std::vector<AnimationChannel> channels_;
|
||||
std::vector<const Transform*> frame_refs_;
|
||||
|
||||
@ -40,6 +40,8 @@ std::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std
|
||||
}
|
||||
});
|
||||
|
||||
skeleton->AddAimBones();
|
||||
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
@ -95,3 +97,24 @@ void assets::Skeleton::AddAnimation(const std::string& name, const std::shared_p
|
||||
anim_idxs_[name] = anims_.size();
|
||||
anims_.push_back(anim);
|
||||
}
|
||||
|
||||
void assets::Skeleton::AddAimBones()
|
||||
{
|
||||
AddAimBone("DEF-spine.002", 0.5f);
|
||||
AddAimBone("MCH-spine.002", 0.5f);
|
||||
AddAimBone("DEF-spine.003", 0.5f);
|
||||
AddAimBone("MCH-spine.003", 0.5f);
|
||||
|
||||
}
|
||||
|
||||
void assets::Skeleton::AddAimBone(const std::string& name, float weight)
|
||||
{
|
||||
auto idx = GetBoneIndex(name);
|
||||
if (idx < 0)
|
||||
return;
|
||||
|
||||
AimBone aimbone{};
|
||||
aimbone.idx = idx;
|
||||
aimbone.weight = weight;
|
||||
aim_bones_.emplace_back(aimbone);
|
||||
}
|
||||
|
||||
@ -22,6 +22,12 @@ struct Bone
|
||||
using AnimIdx = uint8_t;
|
||||
constexpr AnimIdx NO_ANIM = 255;
|
||||
|
||||
struct AimBone
|
||||
{
|
||||
size_t idx;
|
||||
float weight;
|
||||
};
|
||||
|
||||
class Skeleton
|
||||
{
|
||||
public:
|
||||
@ -37,16 +43,24 @@ public:
|
||||
const Animation* GetAnimation(AnimIdx idx) const;
|
||||
const Animation* GetAnimation(const std::string& name) const;
|
||||
|
||||
const std::vector<AimBone> GetAimBones() const { return aim_bones_; }
|
||||
|
||||
private:
|
||||
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
|
||||
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim);
|
||||
|
||||
void AddAimBones();
|
||||
void AddAimBone(const std::string& name, float weight);
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::vector<Bone> bones_;
|
||||
std::map<std::string, int> bone_map_;
|
||||
|
||||
std::vector<std::shared_ptr<const Animation>> anims_;
|
||||
std::map<std::string, AnimIdx> anim_idxs_;
|
||||
|
||||
std::vector<AimBone> aim_bones_;
|
||||
};
|
||||
|
||||
} // namespace assets
|
||||
@ -196,6 +196,20 @@ static void PollEvents()
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
{
|
||||
if (event.button.button == SDL_BUTTON_LEFT)
|
||||
{
|
||||
s_app->Input(game::IN_ATTACK_PRIMARY, event.button.state == SDL_PRESSED, event.button.clicks > 1);
|
||||
}
|
||||
else if (event.button.button == SDL_BUTTON_RIGHT)
|
||||
{
|
||||
s_app->Input(game::IN_ATTACK_SECONDARY, event.button.state == SDL_PRESSED, event.button.clicks > 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -362,16 +376,6 @@ static void Frame()
|
||||
int width, height;
|
||||
SDL_GetWindowSize(s_window, &width, &height);
|
||||
s_app->SetViewportSize(width, height);
|
||||
|
||||
game::PlayerInputFlags input = 0;
|
||||
const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr);
|
||||
|
||||
|
||||
|
||||
int mouse_state = SDL_GetMouseState(nullptr, nullptr);
|
||||
|
||||
if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT))
|
||||
input |= (1 << game::IN_ATTACK);
|
||||
|
||||
s_app->Frame();
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ game::Animal::Animal(World& world, const CharacterTuning& tuning, const glm::vec
|
||||
SetPosition(position);
|
||||
SetYaw(yaw);
|
||||
EnablePhysics(true);
|
||||
SetMovementType(CMT_TURN);
|
||||
|
||||
collision::AddObjectFlags(&GetController()->GetBtGhost(), collision::OF_USABLE);
|
||||
}
|
||||
@ -40,9 +41,9 @@ void game::Animal::SetRideableInput(PlayerInputFlags in)
|
||||
SetInputs(MapPlayerInputToCharacterInput(in));
|
||||
}
|
||||
|
||||
void game::Animal::SetRideableYaw(float yaw)
|
||||
void game::Animal::SetRideableViewAngles(float yaw, float pitch)
|
||||
{
|
||||
SetForwardYaw(yaw);
|
||||
SetViewAngles(yaw, pitch);
|
||||
}
|
||||
|
||||
void game::Animal::OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger)
|
||||
|
||||
@ -16,7 +16,7 @@ public:
|
||||
virtual void Use(PlayerCharacter& character, uint32_t target_id) override;
|
||||
|
||||
virtual void SetRideableInput(PlayerInputFlags in) override;
|
||||
virtual void SetRideableYaw(float yaw) override;
|
||||
virtual void SetRideableViewAngles(float yaw, float pitch) override;
|
||||
|
||||
protected:
|
||||
virtual void OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) override;
|
||||
|
||||
@ -38,6 +38,7 @@ void game::Character::Update()
|
||||
|
||||
SyncTransformFromController();
|
||||
UpdateMovement();
|
||||
UpdateActionAnim();
|
||||
root_.UpdateMatrix();
|
||||
|
||||
sync_current_ = 1 - sync_current_;
|
||||
@ -100,6 +101,17 @@ void game::Character::SetInput(CharacterInputType type, bool enable)
|
||||
in_ &= ~(1 << type);
|
||||
}
|
||||
|
||||
void game::Character::SetMovementType(CharacterMovementType type)
|
||||
{
|
||||
movement_ = type;
|
||||
}
|
||||
|
||||
void game::Character::SetViewAngles(float yaw, float pitch)
|
||||
{
|
||||
view_yaw_ = yaw;
|
||||
view_pitch_ = pitch;
|
||||
}
|
||||
|
||||
void game::Character::SetPosition(const glm::vec3& position)
|
||||
{
|
||||
root_.local.position = position;
|
||||
@ -121,6 +133,30 @@ void game::Character::SetRunAnim(const std::string& anim_name)
|
||||
animstate_.run_anim_idx = GetAnim(anim_name);
|
||||
}
|
||||
|
||||
void game::Character::PlayActionAnim(assets::AnimIdx anim_idx, float speed)
|
||||
{
|
||||
action_anim_end_ = (anim_idx != assets::NO_ANIM) ? sk_.GetSkeleton()->GetAnimation(anim_idx)->GetDuration() : 0.0f;
|
||||
|
||||
if (animstate_.action_anim_idx != anim_idx)
|
||||
{
|
||||
// continue from current time if same anim
|
||||
animstate_.action_phase = (speed > 0.0f) ? 0.0f : action_anim_end_;
|
||||
}
|
||||
animstate_.action_anim_idx = anim_idx;
|
||||
action_anim_playback_speed_ = speed;
|
||||
action_anim_done_ = anim_idx == assets::NO_ANIM;
|
||||
}
|
||||
|
||||
void game::Character::PlayActionAnim(const std::string& anim_name, float speed)
|
||||
{
|
||||
PlayActionAnim(GetAnim(anim_name), speed);
|
||||
}
|
||||
|
||||
void game::Character::ClearActionAnim()
|
||||
{
|
||||
PlayActionAnim(assets::NO_ANIM, 0.0f);
|
||||
}
|
||||
|
||||
void game::Character::SyncControllerTransform()
|
||||
{
|
||||
if (!controller_)
|
||||
@ -143,42 +179,58 @@ void game::Character::SyncTransformFromController()
|
||||
root_.local.position.z -= z_offset_; // foot pos
|
||||
}
|
||||
|
||||
static glm::vec2 GetInputDir(game::CharacterInputFlags in)
|
||||
{
|
||||
glm::vec2 dir(0.0f);
|
||||
|
||||
if (in & (1 << game::CIN_FORWARD))
|
||||
dir.y += 1.0f;
|
||||
|
||||
if (in & (1 << game::CIN_BACKWARD))
|
||||
dir.y -= 1.0f;
|
||||
|
||||
if (in & (1 << game::CIN_RIGHT))
|
||||
dir.x -= 1.0f;
|
||||
|
||||
if (in & (1 << game::CIN_LEFT))
|
||||
dir.x += 1.0f;
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
void game::Character::UpdateMovement()
|
||||
{
|
||||
if (movement_ == CMT_DISABLED)
|
||||
{
|
||||
animstate_.loco_blend = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float dt = 1.0f / 25.0f;
|
||||
bool walking = false;
|
||||
bool running = false;
|
||||
glm::vec2 movedir(0.0f);
|
||||
|
||||
if (in_ & (1 << CIN_FORWARD))
|
||||
movedir.y += 1.0f;
|
||||
glm::vec3 move_dir(0.0f);
|
||||
|
||||
if (in_ & (1 << CIN_BACKWARD))
|
||||
movedir.y -= 1.0f;
|
||||
|
||||
if (in_ & (1 << CIN_RIGHT))
|
||||
movedir.x -= 1.0f;
|
||||
|
||||
if (in_ & (1 << CIN_LEFT))
|
||||
movedir.x += 1.0f;
|
||||
|
||||
glm::vec3 walkdir(0.0f);
|
||||
|
||||
if (movedir.x != 0.0f || movedir.y != 0.0f)
|
||||
auto input_dir = GetInputDir(in_);
|
||||
if (input_dir.x != 0.0f || input_dir.y != 0.0f)
|
||||
{
|
||||
walking = true;
|
||||
|
||||
if (in_ & (1 << CIN_SPRINT))
|
||||
running = true;
|
||||
|
||||
float target_yaw = forward_yaw_ + std::atan2(movedir.x, movedir.y);
|
||||
Turn(yaw_, target_yaw, turn_speed_ * dt);
|
||||
|
||||
glm::vec3 forward_dir(-glm::sin(yaw_), glm::cos(yaw_), 0.0f);
|
||||
walkdir = forward_dir * walk_speed_ * dt;
|
||||
const bool directional = (movement_ == CMT_DIRECTIONAL);
|
||||
|
||||
float relative_yaw = std::atan2(input_dir.x, input_dir.y);
|
||||
float turn_yaw = directional ? view_yaw_ : view_yaw_ + relative_yaw;
|
||||
Turn(yaw_, turn_yaw, turn_speed_ * dt);
|
||||
float move_yaw = directional ? yaw_ + relative_yaw : yaw_;
|
||||
|
||||
move_dir = glm::vec3(-glm::sin(move_yaw), glm::cos(move_yaw), 0.0f) * walk_speed_ * dt;
|
||||
|
||||
if (running)
|
||||
walkdir *= run_speed_mult_;
|
||||
move_dir *= run_speed_mult_;
|
||||
|
||||
}
|
||||
|
||||
@ -187,7 +239,7 @@ void game::Character::UpdateMovement()
|
||||
if (controller_)
|
||||
{
|
||||
auto& bt_character = controller_->GetBtController();
|
||||
bt_character.setWalkDirection(btVector3(walkdir.x, walkdir.y, walkdir.z));
|
||||
bt_character.setWalkDirection(btVector3(move_dir.x, move_dir.y, move_dir.z));
|
||||
|
||||
if (in_ & (1 << CIN_JUMP) && bt_character.canJump())
|
||||
{
|
||||
@ -202,6 +254,9 @@ void game::Character::UpdateMovement()
|
||||
if (running)
|
||||
anim_speed *= run_speed_mult_;
|
||||
animstate_.loco_phase = glm::mod(animstate_.loco_phase + anim_speed * dt, 1.0f);
|
||||
|
||||
animstate_.pitch = view_pitch_;
|
||||
|
||||
}
|
||||
|
||||
void game::Character::UpdateSyncState()
|
||||
@ -220,6 +275,14 @@ void game::Character::UpdateSyncState()
|
||||
state.run_anim = animstate_.run_anim_idx;
|
||||
state.loco_phase.Encode(animstate_.loco_phase);
|
||||
state.loco_blend.Encode(animstate_.loco_blend);
|
||||
|
||||
// action
|
||||
state.action_anim = animstate_.action_anim_idx;
|
||||
state.action_phase.Encode(animstate_.action_phase);
|
||||
|
||||
// aim
|
||||
state.aim_yaw.Encode(animstate_.yaw);
|
||||
state.aim_pitch.Encode(animstate_.pitch);
|
||||
}
|
||||
|
||||
void game::Character::SendUpdateMsg()
|
||||
@ -282,6 +345,31 @@ game::CharacterSyncFieldFlags game::Character::WriteState(net::OutMessage& msg,
|
||||
net::WriteDelta(msg, curr.loco_phase, base.loco_phase);
|
||||
}
|
||||
|
||||
// action anim
|
||||
if (curr.action_anim != base.action_anim)
|
||||
{
|
||||
fields |= CSF_ACTION_ANIM;
|
||||
|
||||
msg.Write(curr.action_anim);
|
||||
}
|
||||
|
||||
// action phase
|
||||
if (curr.action_phase.value != base.action_phase.value)
|
||||
{
|
||||
fields |= CSF_ACTION_PHASE;
|
||||
|
||||
net::WriteDelta(msg, curr.action_phase, base.action_phase);
|
||||
}
|
||||
|
||||
// aim
|
||||
if (curr.aim_yaw.value != base.aim_yaw.value || curr.aim_pitch.value != base.aim_pitch.value)
|
||||
{
|
||||
fields |= CSF_AIM;
|
||||
|
||||
net::WriteDelta(msg, curr.aim_yaw.value, base.aim_yaw.value);
|
||||
net::WriteDelta(msg, curr.aim_pitch.value, base.aim_pitch.value);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -290,6 +378,31 @@ assets::AnimIdx game::Character::GetAnim(const std::string& name) const
|
||||
return sk_.GetSkeleton()->GetAnimationIdx(name);
|
||||
}
|
||||
|
||||
void game::Character::UpdateActionAnim()
|
||||
{
|
||||
if (action_anim_done_)
|
||||
return;
|
||||
|
||||
animstate_.action_phase += action_anim_playback_speed_ * (1.0f / 25.0f);
|
||||
|
||||
if (action_anim_playback_speed_ > 0.0f)
|
||||
{
|
||||
if (animstate_.action_phase >= action_anim_end_)
|
||||
{
|
||||
animstate_.action_phase = action_anim_end_;
|
||||
action_anim_done_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (animstate_.action_phase <= 0.0f)
|
||||
{
|
||||
animstate_.action_phase = 0.0f;
|
||||
action_anim_done_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
game::CharacterPhysicsController::CharacterPhysicsController(Character& character, btDynamicsWorld& bt_world, btCapsuleShapeZ& bt_shape)
|
||||
: character_(character), bt_world_(bt_world), bt_character_(&bt_ghost_, &bt_shape, 0.3f, btVector3(0, 0, 1))
|
||||
{
|
||||
|
||||
@ -45,6 +45,13 @@ private:
|
||||
btKinematicCharacterController bt_character_;
|
||||
};
|
||||
|
||||
enum CharacterMovementType
|
||||
{
|
||||
CMT_DISABLED,
|
||||
CMT_TURN,
|
||||
CMT_DIRECTIONAL,
|
||||
};
|
||||
|
||||
class Character : public Entity
|
||||
{
|
||||
public:
|
||||
@ -64,13 +71,17 @@ public:
|
||||
|
||||
void SetInput(CharacterInputType type, bool enable);
|
||||
void SetInputs(CharacterInputFlags inputs) { in_ = inputs; }
|
||||
CharacterInputFlags GetInputs() const { return in_; }
|
||||
|
||||
void SetMovementType(CharacterMovementType type);
|
||||
|
||||
void SetViewAngles(float yaw, float pitch);
|
||||
float GetViewYaw() const { return view_yaw_; }
|
||||
float GetViewPitch() const { return view_pitch_; }
|
||||
|
||||
void SetForwardYaw(float yaw) { forward_yaw_ = yaw; }
|
||||
float GetForwardYaw() const { return forward_yaw_; }
|
||||
void SetYaw(float yaw) { yaw_ = yaw; }
|
||||
|
||||
void SetPosition(const glm::vec3& position);
|
||||
|
||||
|
||||
~Character() override = default;
|
||||
|
||||
@ -78,6 +89,10 @@ protected:
|
||||
void SetIdleAnim(const std::string& anim_name);
|
||||
void SetWalkAnim(const std::string& anim_name);
|
||||
void SetRunAnim(const std::string& anim_name);
|
||||
void PlayActionAnim(assets::AnimIdx anim_idx, float speed);
|
||||
void PlayActionAnim(const std::string& anim_name, float speed = 1.0f);
|
||||
void ClearActionAnim();
|
||||
bool IsActionAnimDone() { return action_anim_done_; }
|
||||
|
||||
private:
|
||||
void SyncControllerTransform();
|
||||
@ -90,6 +105,8 @@ private:
|
||||
|
||||
assets::AnimIdx GetAnim(const std::string& name) const;
|
||||
|
||||
void UpdateActionAnim();
|
||||
|
||||
protected:
|
||||
float turn_speed_ = 8.0f;
|
||||
float walk_speed_ = 2.0f;
|
||||
@ -108,13 +125,20 @@ private:
|
||||
std::unique_ptr<CharacterPhysicsController> controller_;
|
||||
|
||||
float yaw_ = 0.0f;
|
||||
float forward_yaw_ = 0.0f;
|
||||
float view_yaw_ = 0.0f;
|
||||
float view_pitch_ = 0.0f;
|
||||
|
||||
SkeletonInstance sk_;
|
||||
CharacterAnimState animstate_;
|
||||
|
||||
CharacterSyncState sync_[2];
|
||||
size_t sync_current_ = 0;
|
||||
|
||||
CharacterMovementType movement_ = CMT_DISABLED;
|
||||
|
||||
float action_anim_playback_speed_ = 0.0f;
|
||||
float action_anim_end_ = 0.0f;
|
||||
bool action_anim_done_ = true;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
@ -26,7 +26,6 @@ void game::CharacterAnimState::ApplyToSkeleton(SkeletonInstance& sk) const
|
||||
sk.ApplySkelAnim(*idle_anim, loco_phase, 1.0f);
|
||||
sk.ApplySkelAnim(*walk_anim, loco_phase, UnMix(0.0f, 0.5f, loco_blend));
|
||||
}
|
||||
|
||||
else if (loco_blend == 0.5f) // walk
|
||||
{
|
||||
sk.ApplySkelAnim(*walk_anim, loco_phase, 1.0f);
|
||||
@ -41,4 +40,16 @@ void game::CharacterAnimState::ApplyToSkeleton(SkeletonInstance& sk) const
|
||||
sk.ApplySkelAnim(*run_anim, loco_phase, 1.0f);
|
||||
}
|
||||
|
||||
// action
|
||||
auto action_anim = skeleton->GetAnimation(action_anim_idx);
|
||||
if (action_anim)
|
||||
{
|
||||
sk.ApplySkelAnim(*action_anim, action_phase, 1.0f);
|
||||
}
|
||||
|
||||
if (glm::abs(yaw) > 0.01f || glm::abs(pitch) > 0.01f)
|
||||
{
|
||||
sk.ApplyAim(yaw, pitch);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,12 +8,18 @@ namespace game
|
||||
struct CharacterAnimState
|
||||
{
|
||||
assets::AnimIdx idle_anim_idx = assets::NO_ANIM;
|
||||
|
||||
assets::AnimIdx walk_anim_idx = assets::NO_ANIM;
|
||||
assets::AnimIdx run_anim_idx = assets::NO_ANIM;
|
||||
|
||||
float loco_blend = 0.0f;
|
||||
float loco_phase = 0.0f;
|
||||
|
||||
assets::AnimIdx action_anim_idx = assets::NO_ANIM;
|
||||
float action_phase = 0.0f;
|
||||
|
||||
float yaw = 0.0f;
|
||||
float pitch = 0.0f;
|
||||
|
||||
void ApplyToSkeleton(SkeletonInstance& sk) const;
|
||||
|
||||
|
||||
|
||||
@ -22,20 +22,30 @@ struct CharacterSyncState
|
||||
assets::AnimIdx run_anim = assets::NO_ANIM;
|
||||
net::AnimBlendQ loco_blend;
|
||||
net::AnimTimeQ loco_phase;
|
||||
|
||||
// action anim
|
||||
assets::AnimIdx action_anim = assets::NO_ANIM;
|
||||
net::AnimTimeQ action_phase;
|
||||
|
||||
// aim
|
||||
net::AnimAimAngleQ aim_yaw;
|
||||
net::AnimAimAngleQ aim_pitch;
|
||||
|
||||
//assets::AnimIdx strafe_left_anim = assets::NO_ANIM;
|
||||
//assets::AnimIdx strafe_right_anim = assets::NO_ANIM;
|
||||
|
||||
// TODO: action
|
||||
};
|
||||
|
||||
using CharacterSyncFieldFlags = uint8_t;
|
||||
|
||||
enum CharacterSyncFieldFlag
|
||||
{
|
||||
CSF_TRANSFORM = 0x01,
|
||||
CSF_IDLE_ANIM = 0x02,
|
||||
CSF_LOCO_ANIMS = 0x04,
|
||||
CSF_LOCO_VALS = 0x08,
|
||||
CSF_TRANSFORM = 1,
|
||||
CSF_IDLE_ANIM = 2,
|
||||
CSF_LOCO_ANIMS = 4,
|
||||
CSF_LOCO_VALS = 8,
|
||||
CSF_ACTION_ANIM = 16,
|
||||
CSF_ACTION_PHASE = 32,
|
||||
CSF_AIM = 64,
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
@ -45,7 +45,7 @@ void game::EnterableWorld::PlayerViewAnglesChanged(Player& player, float yaw, fl
|
||||
|
||||
auto character = it->second;
|
||||
|
||||
character->SetForwardYaw(yaw);
|
||||
character->SetViewAngles(yaw, pitch);
|
||||
}
|
||||
|
||||
void game::EnterableWorld::RemovePlayer(Player& player)
|
||||
|
||||
@ -17,33 +17,27 @@ game::HumanCharacter::HumanCharacter(World& world, const HumanCharacterTuning& t
|
||||
SetWalkAnim("walk");
|
||||
}
|
||||
|
||||
void game::HumanCharacter::Update()
|
||||
{
|
||||
UpdateState();
|
||||
UpdateActionState();
|
||||
Super::Update();
|
||||
}
|
||||
|
||||
void game::HumanCharacter::SetRideable(Rideable* rideable, size_t seat_idx)
|
||||
{
|
||||
if (rideable == rideable_ && seat_idx == seat_idx_)
|
||||
return;
|
||||
|
||||
if (rideable)
|
||||
|
||||
if (rideable_)
|
||||
{
|
||||
SetPosition(rideable->GetSeatOffset(seat_idx));
|
||||
EnablePhysics(false);
|
||||
|
||||
Attach(rideable->GetEntity().GetEntNum());
|
||||
SetIdleAnim((rideable->GetRideableType() == RIDEABLE_VEHICLE && seat_idx == 0) ? "vehicle_drive" : "vehicle_passenger");
|
||||
SetYaw(0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnablePhysics(true);
|
||||
|
||||
glm::vec3 seat_loc = rideable_->GetSeatOffset(seat_idx_);
|
||||
seat_loc.x += glm::sign(seat_loc.x) * 0.5f; // to the side
|
||||
|
||||
glm::vec3 pos = rideable_->GetEntity().GetRoot().matrix * glm::vec4(seat_loc, 1.0f);
|
||||
pos.z += 0.5f;
|
||||
SetPosition(pos);
|
||||
|
||||
Attach(0);
|
||||
SetIdleAnim("idle");
|
||||
rideable_exit_pos_ = pos;
|
||||
}
|
||||
|
||||
rideable_ = rideable;
|
||||
@ -51,8 +45,10 @@ void game::HumanCharacter::SetRideable(Rideable* rideable, size_t seat_idx)
|
||||
seat_idx_ = seat_idx;
|
||||
is_driver_ = rideable && seat_idx_ == 0;
|
||||
|
||||
SetSignal(HSS_RIDEABLE_CHANGED);
|
||||
OnRideableChanged();
|
||||
|
||||
|
||||
}
|
||||
|
||||
void game::HumanCharacter::Ride(Rideable* rideable, size_t seat_idx)
|
||||
@ -72,3 +68,238 @@ game::HumanCharacter::~HumanCharacter()
|
||||
{
|
||||
Ride(nullptr, 0); // exit rideable
|
||||
}
|
||||
|
||||
void game::HumanCharacter::UpdateState()
|
||||
{
|
||||
struct HumanCharacterStateTableEntry
|
||||
{
|
||||
void (HumanCharacter::*enter)();
|
||||
HumanCharacterState (HumanCharacter::*update)();
|
||||
void (HumanCharacter::*exit)();
|
||||
};
|
||||
|
||||
static const HumanCharacterStateTableEntry state_table[] =
|
||||
{
|
||||
// HS_INIT
|
||||
{
|
||||
nullptr,
|
||||
&HumanCharacter::StateInitUpdate,
|
||||
nullptr,
|
||||
},
|
||||
|
||||
// HS_ON_FOOT
|
||||
{
|
||||
&HumanCharacter::StateOnFootEnter,
|
||||
&HumanCharacter::StateOnFootUpdate,
|
||||
nullptr,
|
||||
},
|
||||
|
||||
// HS_RIDING
|
||||
{
|
||||
&HumanCharacter::StateRidingEnter,
|
||||
&HumanCharacter::StateRidingUpdate,
|
||||
&HumanCharacter::StateRidingExit,
|
||||
},
|
||||
|
||||
// HS_KNOCKED_DOWN
|
||||
{
|
||||
nullptr,
|
||||
&HumanCharacter::StateKnockedDownUpdate,
|
||||
nullptr,
|
||||
},
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
auto new_state = (this->*state_table[state_].update)();
|
||||
|
||||
if (new_state == state_)
|
||||
break;
|
||||
|
||||
if (auto exit_fun = state_table[state_].exit)
|
||||
(this->*exit_fun)();
|
||||
|
||||
state_ = new_state;
|
||||
|
||||
if (auto enter_fun = state_table[state_].enter)
|
||||
(this->*enter_fun)();
|
||||
}
|
||||
|
||||
signals_ = 0;
|
||||
}
|
||||
|
||||
void game::HumanCharacter::SetSignal(HumanCharacterStateSignal signal)
|
||||
{
|
||||
signals_ |= signal;
|
||||
}
|
||||
|
||||
bool game::HumanCharacter::PopSignal(HumanCharacterStateSignal signal)
|
||||
{
|
||||
if (signals_ & signal)
|
||||
{
|
||||
signals_ &= ~signal;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
game::HumanCharacterState game::HumanCharacter::StateInitUpdate()
|
||||
{
|
||||
if (GetRideable())
|
||||
return HS_RIDING;
|
||||
|
||||
return HS_ON_FOOT;
|
||||
}
|
||||
|
||||
void game::HumanCharacter::StateOnFootEnter()
|
||||
{
|
||||
SetIdleAnim("idle");
|
||||
SetWalkAnim("walk");
|
||||
SetMovementType(CMT_TURN);
|
||||
EnablePhysics(true);
|
||||
}
|
||||
|
||||
game::HumanCharacterState game::HumanCharacter::StateOnFootUpdate()
|
||||
{
|
||||
if (PopSignal(HSS_RIDEABLE_CHANGED))
|
||||
return HS_INIT;
|
||||
|
||||
if (PopSignal(HSS_KNOCK_DOWN))
|
||||
return HS_KNOCKED_DOWN;
|
||||
|
||||
SetMovementType(aiming_ ? CMT_DIRECTIONAL : CMT_TURN);
|
||||
|
||||
return HS_ON_FOOT;
|
||||
}
|
||||
|
||||
void game::HumanCharacter::StateRidingEnter()
|
||||
{
|
||||
auto rideable = GetRideable();
|
||||
SetPosition(rideable->GetSeatOffset(seat_idx_));
|
||||
EnablePhysics(false);
|
||||
|
||||
Attach(rideable->GetEntity().GetEntNum());
|
||||
SetIdleAnim((rideable->GetRideableType() == RIDEABLE_VEHICLE && seat_idx_ == 0) ? "vehicle_drive" : "vehicle_passenger");
|
||||
SetYaw(0.0f);
|
||||
SetMovementType(CMT_DISABLED);
|
||||
}
|
||||
|
||||
game::HumanCharacterState game::HumanCharacter::StateRidingUpdate()
|
||||
{
|
||||
if (!GetRideable())
|
||||
return HS_INIT;
|
||||
|
||||
if (PopSignal(HSS_RIDEABLE_CHANGED))
|
||||
return HS_INIT;
|
||||
|
||||
return HS_RIDING;
|
||||
}
|
||||
|
||||
void game::HumanCharacter::StateRidingExit()
|
||||
{
|
||||
EnablePhysics(true);
|
||||
SetPosition(rideable_exit_pos_);
|
||||
Attach(0);
|
||||
}
|
||||
|
||||
game::HumanCharacterState game::HumanCharacter::StateKnockedDownUpdate()
|
||||
{
|
||||
return HS_INIT;
|
||||
}
|
||||
|
||||
void game::HumanCharacter::UpdateActionState()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto new_state = CheckActionStateTransition();
|
||||
|
||||
if (new_state == actionstate_)
|
||||
break;
|
||||
|
||||
ExitActionState();
|
||||
actionstate_ = new_state;
|
||||
EnterActionState();
|
||||
}
|
||||
}
|
||||
|
||||
void game::HumanCharacter::EnterActionState()
|
||||
{
|
||||
switch (actionstate_)
|
||||
{
|
||||
case ACTION_IDLE:
|
||||
ClearActionAnim();
|
||||
break;
|
||||
|
||||
case ACTION_AIM:
|
||||
PlayActionAnim("rifle_aim", 3.0f);
|
||||
break;
|
||||
|
||||
case ACTION_AIMING:
|
||||
break;
|
||||
|
||||
case ACTION_FIRE:
|
||||
PlayActionAnim("rifle_fire");
|
||||
break;
|
||||
|
||||
case ACTION_UNAIM:
|
||||
PlayActionAnim("rifle_aim", -3.0f);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
{
|
||||
switch (actionstate_)
|
||||
{
|
||||
case ACTION_IDLE:
|
||||
if (aiming_) // want aim
|
||||
return ACTION_AIM;
|
||||
|
||||
return ACTION_IDLE;
|
||||
|
||||
case ACTION_AIM:
|
||||
if (IsActionAnimDone())
|
||||
return ACTION_AIMING;
|
||||
|
||||
if (!aiming_) // stop aiming immediately
|
||||
return ACTION_UNAIM;
|
||||
|
||||
return ACTION_AIM;
|
||||
|
||||
case ACTION_AIMING:
|
||||
if (!aiming_)
|
||||
return ACTION_UNAIM; // wants aim no more
|
||||
|
||||
// TODO: check fire
|
||||
|
||||
return ACTION_AIMING;
|
||||
|
||||
case ACTION_FIRE:
|
||||
return ACTION_FIRE;
|
||||
|
||||
case ACTION_UNAIM:
|
||||
if (IsActionAnimDone())
|
||||
return ACTION_IDLE;
|
||||
|
||||
if (aiming_) // start aiming again
|
||||
return ACTION_AIM;
|
||||
|
||||
return ACTION_UNAIM;
|
||||
|
||||
default:
|
||||
return actionstate_;
|
||||
}
|
||||
}
|
||||
|
||||
void game::HumanCharacter::ExitActionState()
|
||||
{
|
||||
switch (actionstate_)
|
||||
{
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,31 @@ struct HumanCharacterTuning
|
||||
class Rideable;
|
||||
class DrivableVehicle;
|
||||
|
||||
using HumanCharacterStateSignals = uint32_t;
|
||||
|
||||
enum HumanCharacterStateSignal : HumanCharacterStateSignals
|
||||
{
|
||||
HSS_KNOCK_DOWN = 1,
|
||||
HSS_RIDEABLE_CHANGED = 2,
|
||||
};
|
||||
|
||||
enum HumanCharacterState
|
||||
{
|
||||
HS_INIT,
|
||||
HS_ON_FOOT,
|
||||
HS_RIDING,
|
||||
HS_KNOCKED_DOWN,
|
||||
};
|
||||
|
||||
enum ActionState
|
||||
{
|
||||
ACTION_IDLE,
|
||||
ACTION_AIM,
|
||||
ACTION_AIMING,
|
||||
ACTION_FIRE,
|
||||
ACTION_UNAIM,
|
||||
};
|
||||
|
||||
class HumanCharacter : public Character
|
||||
{
|
||||
public:
|
||||
@ -20,6 +45,8 @@ public:
|
||||
|
||||
HumanCharacter(World& world, const HumanCharacterTuning& tuning);
|
||||
|
||||
virtual void Update() override;
|
||||
|
||||
const HumanCharacterTuning& GetHumanTuning() const { return human_tuning_; }
|
||||
|
||||
void SetRideable(Rideable* rideable, size_t seat_idx); // called by Rideable!!
|
||||
@ -31,11 +58,41 @@ public:
|
||||
size_t GeatSeatIdx() const { return seat_idx_; }
|
||||
bool IsDriver() const { return is_driver_; }
|
||||
|
||||
void SetAiming(bool aiming) { aiming_ = aiming; }
|
||||
|
||||
virtual ~HumanCharacter() override;
|
||||
|
||||
protected:
|
||||
virtual void OnRideableChanged() {}
|
||||
|
||||
private:
|
||||
void UpdateState();
|
||||
void SetSignal(HumanCharacterStateSignal signal);
|
||||
bool PopSignal(HumanCharacterStateSignal signal);
|
||||
|
||||
//void StateInitEnter();
|
||||
HumanCharacterState StateInitUpdate();
|
||||
//void StateInitExit();
|
||||
|
||||
void StateOnFootEnter();
|
||||
HumanCharacterState StateOnFootUpdate();
|
||||
//void StateOnFootExit();
|
||||
|
||||
void StateRidingEnter();
|
||||
HumanCharacterState StateRidingUpdate();
|
||||
void StateRidingExit();
|
||||
|
||||
// void StateKnockedDownEnter();
|
||||
HumanCharacterState StateKnockedDownUpdate();
|
||||
// void StateKnockedDownExit();
|
||||
|
||||
|
||||
void UpdateActionState();
|
||||
|
||||
void EnterActionState();
|
||||
ActionState CheckActionStateTransition();
|
||||
void ExitActionState();
|
||||
|
||||
private:
|
||||
HumanCharacterTuning human_tuning_;
|
||||
|
||||
@ -43,6 +100,16 @@ private:
|
||||
DrivableVehicle* vehicle_ = nullptr;
|
||||
size_t seat_idx_ = 0;
|
||||
bool is_driver_ = false;
|
||||
|
||||
HumanCharacterState state_ = HS_INIT;
|
||||
HumanCharacterStateSignals signals_ = 0;
|
||||
|
||||
glm::vec3 rideable_exit_pos_ = glm::vec3(0.0f);
|
||||
|
||||
bool aiming_ = false;
|
||||
|
||||
ActionState actionstate_ = ACTION_IDLE;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ void game::PlayerCharacter::Update()
|
||||
|
||||
if (GetRideable() && IsDriver())
|
||||
{
|
||||
GetRideable()->SetRideableYaw(GetForwardYaw());
|
||||
GetRideable()->SetRideableViewAngles(GetViewYaw(), GetViewPitch());
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,8 @@ void game::PlayerCharacter::UpdateInputs()
|
||||
{
|
||||
SetInputs(MapPlayerInputToCharacterInput(in));
|
||||
}
|
||||
|
||||
SetAiming(in & (1 << IN_ATTACK_SECONDARY));
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::UpdateUseTarget()
|
||||
|
||||
@ -16,7 +16,8 @@ namespace game
|
||||
IN_CROUCH,
|
||||
IN_SPRINT,
|
||||
IN_USE,
|
||||
IN_ATTACK,
|
||||
IN_ATTACK_PRIMARY,
|
||||
IN_ATTACK_SECONDARY,
|
||||
IN_DEBUG1,
|
||||
IN_DEBUG2,
|
||||
IN_DEBUG3,
|
||||
|
||||
@ -31,7 +31,7 @@ public:
|
||||
void KickAll();
|
||||
|
||||
virtual void SetRideableInput(PlayerInputFlags in) {}
|
||||
virtual void SetRideableYaw(float yaw) {}
|
||||
virtual void SetRideableViewAngles(float yaw, float pitch) {}
|
||||
|
||||
RideableType GetRideableType() const { return type_; }
|
||||
|
||||
|
||||
@ -52,6 +52,20 @@ void game::SkeletonInstance::ApplySkelAnim(const assets::Animation& anim, float
|
||||
}
|
||||
}
|
||||
|
||||
void game::SkeletonInstance::ApplyAim(float yaw, float pitch)
|
||||
{
|
||||
const auto& aim_bones = skeleton_->GetAimBones();
|
||||
if (aim_bones.empty())
|
||||
return;
|
||||
|
||||
for (const auto& aim_bone : aim_bones)
|
||||
{
|
||||
auto& bone_transform = bone_nodes_[aim_bone.idx].local;
|
||||
auto rotation = glm::angleAxis(-pitch * aim_bone.weight, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
bone_transform.rotation = rotation * bone_transform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
void game::SkeletonInstance::UpdateBoneMatrices()
|
||||
{
|
||||
for (TransformNode& node : bone_nodes_)
|
||||
|
||||
@ -17,6 +17,8 @@ public:
|
||||
const TransformNode& GetBoneNode(size_t index) const { return bone_nodes_[index]; }
|
||||
|
||||
void ApplySkelAnim(const assets::Animation& anim, float time, float weight);
|
||||
void ApplyAim(float yaw, float pitch);
|
||||
|
||||
void UpdateBoneMatrices();
|
||||
|
||||
const std::vector<TransformNode> GetBoneNodes() const { return bone_nodes_; }
|
||||
|
||||
@ -63,10 +63,13 @@ void game::view::CharacterView::Update(const UpdateInfo& info)
|
||||
|
||||
// interpolate states
|
||||
float tps = 25.0f;
|
||||
float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer
|
||||
float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer;
|
||||
t = glm::clamp(t, 0.0f, 2.0f);
|
||||
float t_sane = glm::clamp(t, 0.0f, 1.0f);
|
||||
|
||||
root_.local = Transform::Lerp(states_[0].trans, states_[1].trans, t);
|
||||
|
||||
// loco
|
||||
animstate_.loco_blend = glm::mix(states_[0].loco_blend, states_[1].loco_blend, t);
|
||||
|
||||
float loco_phase0 = states_[0].loco_phase;
|
||||
@ -75,6 +78,13 @@ void game::view::CharacterView::Update(const UpdateInfo& info)
|
||||
loco_phase0 -= 1.0f;
|
||||
animstate_.loco_phase = glm::mod(glm::mix(loco_phase0, loco_phase1, t), 1.0f);
|
||||
|
||||
// action
|
||||
animstate_.action_phase = glm::mix(states_[0].action_phase, states_[1].action_phase, t_sane);
|
||||
|
||||
// aim
|
||||
animstate_.yaw = glm::mix(states_[0].aim_yaw, states_[1].aim_yaw, t_sane);
|
||||
animstate_.pitch = glm::mix(states_[0].aim_pitch, states_[1].aim_pitch, t_sane);
|
||||
|
||||
animstate_.ApplyToSkeleton(sk_);
|
||||
|
||||
root_.UpdateMatrix();
|
||||
@ -157,13 +167,17 @@ void game::view::CharacterView::OnAttach()
|
||||
bool game::view::CharacterView::ReadState(net::InMessage* msg)
|
||||
{
|
||||
update_time_ = world_.GetTime();
|
||||
|
||||
auto& old_state = states_[0];
|
||||
auto& new_state = states_[1];
|
||||
|
||||
// init lerp start state
|
||||
states_[0].trans = root_.local;
|
||||
states_[0].loco_blend = animstate_.loco_blend;
|
||||
states_[0].loco_phase = animstate_.loco_phase;
|
||||
|
||||
auto& new_state = states_[1];
|
||||
old_state.trans = root_.local;
|
||||
old_state.loco_blend = animstate_.loco_blend;
|
||||
old_state.loco_phase = animstate_.loco_phase;
|
||||
old_state.action_phase = animstate_.action_phase;
|
||||
old_state.aim_yaw = animstate_.yaw;
|
||||
old_state.aim_pitch = animstate_.pitch;
|
||||
|
||||
if (msg)
|
||||
{
|
||||
@ -209,6 +223,40 @@ bool game::view::CharacterView::ReadState(net::InMessage* msg)
|
||||
new_state.loco_blend = sync_.loco_blend.Decode();
|
||||
new_state.loco_phase = sync_.loco_phase.Decode();
|
||||
}
|
||||
|
||||
// action anim
|
||||
if (fields & CSF_ACTION_ANIM)
|
||||
{
|
||||
if (!msg->Read(sync_.action_anim))
|
||||
return false;
|
||||
|
||||
animstate_.action_anim_idx = sync_.action_anim;
|
||||
}
|
||||
|
||||
// action phase
|
||||
if (fields & CSF_ACTION_PHASE)
|
||||
{
|
||||
if (!net::ReadDelta(*msg, sync_.action_phase))
|
||||
return false;
|
||||
|
||||
new_state.action_phase = sync_.action_phase.Decode();
|
||||
|
||||
if (fields & CSF_ACTION_ANIM)
|
||||
{
|
||||
// anim just changed, dont blend phase
|
||||
old_state.action_phase = new_state.action_phase;
|
||||
}
|
||||
}
|
||||
|
||||
// aim
|
||||
if (fields & CSF_AIM)
|
||||
{
|
||||
if (!net::ReadDelta(*msg, sync_.aim_yaw) || !net::ReadDelta(*msg, sync_.aim_pitch))
|
||||
return false;
|
||||
|
||||
new_state.aim_yaw = sync_.aim_yaw.Decode();
|
||||
new_state.aim_pitch = sync_.aim_pitch.Decode();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@ -13,8 +13,14 @@ namespace game::view
|
||||
struct CharacterViewState
|
||||
{
|
||||
Transform trans;
|
||||
|
||||
float loco_blend = 0.0f;
|
||||
float loco_phase = 0.0f;
|
||||
|
||||
float action_phase = 0.0f;
|
||||
|
||||
float aim_yaw = 0.0f;
|
||||
float aim_pitch = 0.0f;
|
||||
};
|
||||
|
||||
struct CharacterViewClothes
|
||||
|
||||
@ -139,6 +139,8 @@ using SoundPitchQ = Quantized<uint8_t, 0, 2>;
|
||||
using AnimBlendQ = Quantized<uint8_t, 0, 1>;
|
||||
using AnimTimeQ = Quantized<uint8_t, 0, 1>;
|
||||
|
||||
using AnimAimAngleQ = Quantized<uint8_t, -PI_N, PI_N, PI_D * 2>;
|
||||
|
||||
using NumClothes = uint8_t;
|
||||
using ClothesName = FixedStr<32>;
|
||||
|
||||
|
||||
@ -147,14 +147,14 @@ inline bool ReadRGB(InMessage& msg, uint32_t& color)
|
||||
|
||||
// DELTA
|
||||
template <std::unsigned_integral T> requires (sizeof(T) == 1)
|
||||
inline void WriteDelta(OutMessage& msg, T previous, T current)
|
||||
inline void WriteDelta(OutMessage& msg, T current, T previous)
|
||||
{
|
||||
// 1 byte => just write current
|
||||
msg.Write(current);
|
||||
}
|
||||
|
||||
template <std::unsigned_integral T> requires (sizeof(T) > 1)
|
||||
inline void WriteDelta(OutMessage& msg, T previous, T current)
|
||||
inline void WriteDelta(OutMessage& msg, T current, T previous)
|
||||
{
|
||||
static_assert(sizeof(T) <= 4);
|
||||
|
||||
@ -169,18 +169,18 @@ inline void WriteDelta(OutMessage& msg, T previous, T current)
|
||||
template <AnyQuantized T>
|
||||
inline void WriteDelta(OutMessage& msg, T current, T previous)
|
||||
{
|
||||
WriteDelta(msg, previous.value, current.value);
|
||||
WriteDelta(msg, current.value, previous.value);
|
||||
}
|
||||
|
||||
template <std::unsigned_integral T> requires (sizeof(T) == 1)
|
||||
inline bool ReadDelta(InMessage& msg, T previous, T& current)
|
||||
inline bool ReadDelta(InMessage& msg, T& current, T previous)
|
||||
{
|
||||
// 1 byte => just read current
|
||||
return msg.Read(current);
|
||||
}
|
||||
|
||||
template <std::unsigned_integral T> requires (sizeof(T) > 1)
|
||||
inline bool ReadDelta(InMessage& msg, T previous, T& current)
|
||||
inline bool ReadDelta(InMessage& msg, T& current, T previous)
|
||||
{
|
||||
static_assert(sizeof(T) <= 4);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user