feat(animation): 添加动画状态回调支持
refactor(character): 重构角色动作处理逻辑 feat(swordman): 实现剑士基础攻击和技能1处理 refactor(state): 优化状态机与动作上下文管理 feat(input): 改进输入系统支持动作请求队列 refactor(movement): 重构移动系统支持行走/奔跑模式
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
#include "character/CharacterActionLibrary.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <frostbite2D/resource/script_parser.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
@@ -17,152 +15,47 @@ std::string toLower(const std::string& value) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int toInt(const std::string& value, int fallback = 0) {
|
||||
try {
|
||||
return std::stoi(value);
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
float toFloat(const std::string& value, float fallback = 0.0f) {
|
||||
try {
|
||||
return std::stof(value);
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
bool isTrueToken(const std::string& value) {
|
||||
std::string token = toLower(value);
|
||||
return token == "1" || token == "true" || token == "[true]" || token == "yes";
|
||||
}
|
||||
|
||||
std::string trim(const std::string& value) {
|
||||
size_t begin = 0;
|
||||
size_t end = value.size();
|
||||
while (begin < end && std::isspace(static_cast<unsigned char>(value[begin]))) {
|
||||
++begin;
|
||||
}
|
||||
while (end > begin
|
||||
&& std::isspace(static_cast<unsigned char>(value[end - 1]))) {
|
||||
while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
return value.substr(begin, end - begin);
|
||||
}
|
||||
|
||||
std::string fileStem(const std::string& path) {
|
||||
size_t slashPos = path.find_last_of("/\\");
|
||||
size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1;
|
||||
size_t dotPos = path.find_last_of('.');
|
||||
if (dotPos == std::string::npos || dotPos < begin) {
|
||||
return path.substr(begin);
|
||||
}
|
||||
return path.substr(begin, dotPos - begin);
|
||||
std::string normalizeKey(const std::string& value) {
|
||||
return toLower(trim(value));
|
||||
}
|
||||
|
||||
class ScriptTokenStream {
|
||||
public:
|
||||
explicit ScriptTokenStream(const std::string& path)
|
||||
: path_(PvfArchive::get().normalizePath(path)) {
|
||||
auto rawData = PvfArchive::get().getFileRawData(path_);
|
||||
if (!rawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptParser parser(*rawData, path_);
|
||||
if (!parser.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& value : parser.parseAll()) {
|
||||
tokens_.push_back(value.toString());
|
||||
}
|
||||
valid_ = true;
|
||||
}
|
||||
|
||||
bool IsValid() const { return valid_; }
|
||||
bool IsEnd() const { return index_ >= tokens_.size(); }
|
||||
|
||||
std::string Next() {
|
||||
if (IsEnd()) {
|
||||
return {};
|
||||
}
|
||||
return tokens_[index_++];
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
std::vector<std::string> tokens_;
|
||||
size_t index_ = 0;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
// action_list.lst 同时兼容两种格式:
|
||||
// 1. 标准 PVF #PVF_File + 索引 + `path`
|
||||
// 2. 早期临时版的 actionId + path 成对写法
|
||||
bool isNewActionListPath(const std::string& path) {
|
||||
return path.find("action_logic") != std::string::npos
|
||||
&& path.size() >= 4
|
||||
&& path.substr(path.size() - 4) == ".act";
|
||||
}
|
||||
|
||||
// action_list.lst only accepts #PVF_File entries like `swordman/action_logic/idle.act`.
|
||||
std::vector<std::pair<std::string, std::string>> parseActionListEntries(
|
||||
ScriptTokenStream& listStream) {
|
||||
std::vector<std::pair<std::string, std::string>> entries;
|
||||
std::vector<std::string> tokens;
|
||||
auto& pvf = PvfArchive::get();
|
||||
while (!listStream.IsEnd()) {
|
||||
std::string token = trim(listStream.Next());
|
||||
if (!token.empty()) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i + 1 < tokens.size(); i += 2) {
|
||||
std::string rawActionPath = trim(tokens[i + 1]);
|
||||
std::string actionPath = pvf.normalizePath(rawActionPath);
|
||||
if (actionPath.empty() || !isNewActionListPath(actionPath)) {
|
||||
continue;
|
||||
}
|
||||
entries.push_back({toLower(fileStem(actionPath)), actionPath});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
CharacterFrameEventType parseFrameEventType(const std::string& token) {
|
||||
std::string lower = toLower(token);
|
||||
if (lower == "open_hit_window") {
|
||||
return CharacterFrameEventType::OpenHitWindow;
|
||||
}
|
||||
if (lower == "close_hit_window") {
|
||||
return CharacterFrameEventType::CloseHitWindow;
|
||||
}
|
||||
if (lower == "open_input_buffer") {
|
||||
return CharacterFrameEventType::OpenInputBuffer;
|
||||
}
|
||||
if (lower == "close_input_buffer") {
|
||||
return CharacterFrameEventType::CloseInputBuffer;
|
||||
}
|
||||
if (lower == "set_velocity_xy") {
|
||||
return CharacterFrameEventType::SetVelocityXY;
|
||||
}
|
||||
if (lower == "set_velocity_z") {
|
||||
return CharacterFrameEventType::SetVelocityZ;
|
||||
}
|
||||
if (lower == "finish_action") {
|
||||
return CharacterFrameEventType::FinishAction;
|
||||
}
|
||||
return CharacterFrameEventType::AnimationEvent;
|
||||
}
|
||||
|
||||
void addOrReplaceAction(std::map<std::string, CharacterActionDefinition>& actions,
|
||||
CharacterActionDefinition definition) {
|
||||
if (definition.actionId.empty()) {
|
||||
void addRegisteredAction(std::map<std::string, CharacterActionDefinition>& actions,
|
||||
const std::string& actionId) {
|
||||
std::string key = normalizeKey(actionId);
|
||||
if (key.empty() || actions.count(key) > 0) {
|
||||
return;
|
||||
}
|
||||
actions[definition.actionId] = std::move(definition);
|
||||
|
||||
CharacterActionDefinition definition;
|
||||
definition.actionId = key;
|
||||
actions[key] = std::move(definition);
|
||||
}
|
||||
|
||||
void registerCommonActions(std::map<std::string, CharacterActionDefinition>& actions) {
|
||||
addRegisteredAction(actions, "idle");
|
||||
addRegisteredAction(actions, "move");
|
||||
addRegisteredAction(actions, "run");
|
||||
addRegisteredAction(actions, "jump");
|
||||
addRegisteredAction(actions, "fall");
|
||||
addRegisteredAction(actions, "landing");
|
||||
addRegisteredAction(actions, "hurt_light");
|
||||
}
|
||||
|
||||
void registerSwordmanActions(std::map<std::string, CharacterActionDefinition>& actions) {
|
||||
addRegisteredAction(actions, "attack");
|
||||
addRegisteredAction(actions, "skill_1");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -182,23 +75,34 @@ void CharacterCommandBuffer::Advance(float deltaTime) {
|
||||
void CharacterCommandBuffer::Submit(const CharacterCommand& command) {
|
||||
if (command.type == CharacterCommandType::Move) {
|
||||
moveAxis_ = command.moveAxis;
|
||||
moveMode_ = command.moveMode;
|
||||
if (moveAxis_.lengthSquared() > 1.0f) {
|
||||
moveAxis_ = moveAxis_.normalized();
|
||||
}
|
||||
if (moveAxis_.lengthSquared() < 0.0001f) {
|
||||
moveMode_ = CharacterMoveMode::None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buttons_.push_back({command.type, 0.0f});
|
||||
buttons_.push_back({command.type, command.actionId, 0.0f});
|
||||
}
|
||||
|
||||
CharacterIntent CharacterCommandBuffer::BuildIntent() const {
|
||||
CharacterIntent intent;
|
||||
intent.moveX = moveAxis_.x;
|
||||
intent.moveY = moveAxis_.y;
|
||||
intent.moveMode = moveMode_;
|
||||
intent.wantJump = HasBuffered(CharacterCommandType::JumpPressed);
|
||||
intent.wantAttack = HasBuffered(CharacterCommandType::AttackPressed);
|
||||
intent.wantSkill1 = HasBuffered(CharacterCommandType::Skill1Pressed);
|
||||
intent.wantDash = HasBuffered(CharacterCommandType::DashPressed);
|
||||
for (const auto& button : buttons_) {
|
||||
if (button.type != CharacterCommandType::ActionPressed || button.actionId.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (std::find(intent.requestedActions.begin(), intent.requestedActions.end(),
|
||||
button.actionId) == intent.requestedActions.end()) {
|
||||
intent.requestedActions.push_back(button.actionId);
|
||||
}
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -221,20 +125,61 @@ void CharacterCommandBuffer::Consume(CharacterCommandType type) {
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterCommandBuffer::HasBufferedAction(const std::string& actionId) const {
|
||||
std::string key = normalizeKey(actionId);
|
||||
if (key.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& button : buttons_) {
|
||||
if (button.type == CharacterCommandType::ActionPressed
|
||||
&& normalizeKey(button.actionId) == key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CharacterCommandBuffer::ConsumeAction(const std::string& actionId) {
|
||||
std::string key = normalizeKey(actionId);
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = std::find_if(buttons_.begin(), buttons_.end(),
|
||||
[&key](const BufferedButton& button) {
|
||||
return button.type == CharacterCommandType::ActionPressed
|
||||
&& normalizeKey(button.actionId) == key;
|
||||
});
|
||||
if (it != buttons_.end()) {
|
||||
buttons_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterCommandBuffer::ClearBufferedActions() {
|
||||
buttons_.erase(
|
||||
std::remove_if(buttons_.begin(), buttons_.end(),
|
||||
[](const BufferedButton& button) {
|
||||
return button.type == CharacterCommandType::ActionPressed;
|
||||
}),
|
||||
buttons_.end());
|
||||
}
|
||||
|
||||
void CharacterCommandBuffer::Clear() {
|
||||
moveAxis_ = Vec2::Zero();
|
||||
moveMode_ = CharacterMoveMode::None;
|
||||
buttons_.clear();
|
||||
}
|
||||
|
||||
bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) {
|
||||
actions_.clear();
|
||||
TryLoadPvfActionScripts(config);
|
||||
return !actions_.empty();
|
||||
return BuildExplicitActionRegistry(config);
|
||||
}
|
||||
|
||||
const CharacterActionDefinition* CharacterActionLibrary::FindAction(
|
||||
const std::string& actionId) const {
|
||||
auto it = actions_.find(actionId);
|
||||
std::string key = normalizeKey(actionId);
|
||||
auto it = actions_.find(key);
|
||||
if (it != actions_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
@@ -263,85 +208,15 @@ std::string CharacterActionLibrary::DescribeActionIds() const {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool CharacterActionLibrary::TryLoadPvfActionScripts(
|
||||
bool CharacterActionLibrary::BuildExplicitActionRegistry(
|
||||
const character::CharacterConfig& config) {
|
||||
std::string listPath = "character/" + config.jobTag + "/action_logic/action_list.lst";
|
||||
ScriptTokenStream listStream(listPath);
|
||||
if (!listStream.IsValid()) {
|
||||
return false;
|
||||
registerCommonActions(actions_);
|
||||
|
||||
if (config.jobTag == "swordman") {
|
||||
registerSwordmanActions(actions_);
|
||||
}
|
||||
|
||||
auto entries = parseActionListEntries(listStream);
|
||||
if (entries.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CharacterActionLibrary: action_list parsed 0 entries from %s",
|
||||
listPath.c_str());
|
||||
}
|
||||
for (const auto& [actionId, relativePath] : entries) {
|
||||
if (actionId.empty() || relativePath.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string fullActPath = relativePath.rfind("character/", 0) == 0
|
||||
? relativePath
|
||||
: "character/" + relativePath;
|
||||
ScriptTokenStream actionStream(fullActPath);
|
||||
if (!actionStream.IsValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CharacterActionDefinition definition;
|
||||
definition.actionId = actionId;
|
||||
while (!actionStream.IsEnd()) {
|
||||
std::string token = toLower(actionStream.Next());
|
||||
if (token.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (token == "[action id]") {
|
||||
definition.actionId = toLower(trim(actionStream.Next()));
|
||||
} else if (token == "[animation tag]") {
|
||||
definition.animationTag = trim(actionStream.Next());
|
||||
} else if (token == "[total frames]") {
|
||||
definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames);
|
||||
} else if (token == "[loop]") {
|
||||
definition.loop = isTrueToken(actionStream.Next());
|
||||
} else if (token == "[can move]") {
|
||||
definition.canMove = isTrueToken(actionStream.Next());
|
||||
} else if (token == "[can turn]") {
|
||||
definition.canTurn = isTrueToken(actionStream.Next());
|
||||
} else if (token == "[can be interrupted]") {
|
||||
definition.canBeInterrupted = isTrueToken(actionStream.Next());
|
||||
} else if (token == "[move speed scale]") {
|
||||
definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale);
|
||||
} else if (token == "[cancel rule]") {
|
||||
CharacterCancelRuleDefinition rule;
|
||||
rule.targetAction = toLower(trim(actionStream.Next()));
|
||||
rule.beginFrame = toInt(actionStream.Next(), 0);
|
||||
rule.endFrame = toInt(actionStream.Next(), rule.beginFrame);
|
||||
rule.requireGrounded = isTrueToken(actionStream.Next());
|
||||
rule.requireAirborne = isTrueToken(actionStream.Next());
|
||||
definition.cancelRules.push_back(rule);
|
||||
} else if (token == "[frame event]") {
|
||||
CharacterFrameEventDefinition frameEvent;
|
||||
frameEvent.frame = toInt(actionStream.Next(), 0);
|
||||
frameEvent.type = parseFrameEventType(trim(actionStream.Next()));
|
||||
if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) {
|
||||
frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f);
|
||||
frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f);
|
||||
} else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) {
|
||||
frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f);
|
||||
} else {
|
||||
frameEvent.stringValue = trim(actionStream.Next());
|
||||
}
|
||||
definition.frameEvents.push_back(frameEvent);
|
||||
}
|
||||
}
|
||||
|
||||
addOrReplaceAction(actions_, std::move(definition));
|
||||
}
|
||||
|
||||
return true;
|
||||
return !actions_.empty();
|
||||
}
|
||||
|
||||
std::optional<CharacterActionLibrary> loadCharacterActionLibrary(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "character/CharacterAnimation.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
@@ -38,6 +40,8 @@ bool CharacterAnimation::Init(CharacterObject* parent,
|
||||
actionAnimations_.clear();
|
||||
currentActionTag_.clear();
|
||||
direction_ = 1;
|
||||
actionFrameFlagCallback_ = nullptr;
|
||||
actionEndCallback_ = nullptr;
|
||||
|
||||
for (const auto& [actionName, actionPath] : config.animationPath) {
|
||||
for (const char* slotName : kAvatarParts) {
|
||||
@@ -142,6 +146,56 @@ std::string CharacterAnimation::DescribeAvailableActions() const {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
Animation* CharacterAnimation::GetPrimaryAnimation(const std::string& actionName) const {
|
||||
auto it = actionAnimations_.find(actionName);
|
||||
if (it == actionAnimations_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const auto& animation : it->second) {
|
||||
if (animation && animation->IsUsable()) {
|
||||
return animation.Get();
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& animation : it->second) {
|
||||
if (animation) {
|
||||
return animation.Get();
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Animation* CharacterAnimation::GetCurrentPrimaryAnimation() const {
|
||||
if (currentActionTag_.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return GetPrimaryAnimation(currentActionTag_);
|
||||
}
|
||||
|
||||
void CharacterAnimation::RefreshRuntimeCallbacks() {
|
||||
auto currentIt = actionAnimations_.find(currentActionTag_);
|
||||
if (currentIt == actionAnimations_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Animation* primaryAnimation = GetPrimaryAnimation(currentActionTag_);
|
||||
for (auto& animation : currentIt->second) {
|
||||
if (!animation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (animation.Get() == primaryAnimation) {
|
||||
animation->changeFrameCallback_ = actionFrameFlagCallback_;
|
||||
animation->endCallback_ = actionEndCallback_;
|
||||
} else {
|
||||
animation->changeFrameCallback_ = nullptr;
|
||||
animation->endCallback_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterAnimation::SetAction(const std::string& actionName) {
|
||||
auto nextIt = actionAnimations_.find(actionName);
|
||||
if (nextIt == actionAnimations_.end()) {
|
||||
@@ -172,6 +226,7 @@ bool CharacterAnimation::SetAction(const std::string& actionName) {
|
||||
animation->SetVisible(true);
|
||||
}
|
||||
currentActionTag_ = actionName;
|
||||
RefreshRuntimeCallbacks();
|
||||
SetDirection(direction_);
|
||||
return true;
|
||||
}
|
||||
@@ -191,4 +246,51 @@ void CharacterAnimation::SetDirection(int direction) {
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterAnimation::IsCurrentActionFinished() const {
|
||||
const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
|
||||
if (!primaryAnimation) {
|
||||
return false;
|
||||
}
|
||||
return !primaryAnimation->IsUsable();
|
||||
}
|
||||
|
||||
int CharacterAnimation::GetCurrentFrameIndex() const {
|
||||
const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
|
||||
return primaryAnimation ? primaryAnimation->GetCurrentFrameIndex() : 0;
|
||||
}
|
||||
|
||||
int CharacterAnimation::GetCurrentFrameCount() const {
|
||||
const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
|
||||
return primaryAnimation ? primaryAnimation->GetTotalFrameCount() : 0;
|
||||
}
|
||||
|
||||
float CharacterAnimation::GetCurrentNormalizedProgress() const {
|
||||
int totalFrames = GetCurrentFrameCount();
|
||||
if (totalFrames <= 1) {
|
||||
return IsCurrentActionFinished() ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
float progress = static_cast<float>(GetCurrentFrameIndex())
|
||||
/ static_cast<float>(totalFrames - 1);
|
||||
return std::clamp(progress, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
animation::AniFrame CharacterAnimation::GetCurrentFrameInfo() const {
|
||||
const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
|
||||
if (!primaryAnimation) {
|
||||
return animation::AniFrame();
|
||||
}
|
||||
return primaryAnimation->GetCurrentFrameInfo();
|
||||
}
|
||||
|
||||
void CharacterAnimation::SetActionFrameFlagCallback(ActionFrameFlagCallback callback) {
|
||||
actionFrameFlagCallback_ = std::move(callback);
|
||||
RefreshRuntimeCallbacks();
|
||||
}
|
||||
|
||||
void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
|
||||
actionEndCallback_ = std::move(callback);
|
||||
RefreshRuntimeCallbacks();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -1,45 +1,115 @@
|
||||
#include "character/CharacterInputRouter.h"
|
||||
#include <SDL2/SDL_gamecontroller.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t kKeyboardRunDoubleTapWindowMs = 250;
|
||||
constexpr float kAnalogRunThreshold = 0.75f;
|
||||
|
||||
float applyDeadZone(float value, float deadZone) {
|
||||
return std::fabs(value) >= deadZone ? value : 0.0f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CharacterInputRouter::QueueAction(const std::string& actionId) {
|
||||
if (actionId.empty()) {
|
||||
return;
|
||||
}
|
||||
if (std::find(actionRequests_.begin(), actionRequests_.end(), actionId)
|
||||
== actionRequests_.end()) {
|
||||
actionRequests_.push_back(actionId);
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterInputRouter::IsDirectionPressed(KeyboardMoveDirection direction) const {
|
||||
switch (direction) {
|
||||
case KeyboardMoveDirection::Left:
|
||||
return left_;
|
||||
case KeyboardMoveDirection::Right:
|
||||
return right_;
|
||||
case KeyboardMoveDirection::Up:
|
||||
return up_;
|
||||
case KeyboardMoveDirection::Down:
|
||||
return down_;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterInputRouter::HasDirectionalInput() const {
|
||||
return left_ || right_ || up_ || down_;
|
||||
}
|
||||
|
||||
void CharacterInputRouter::RefreshKeyboardMoveMode() {
|
||||
if (!HasDirectionalInput()) {
|
||||
keyboardMoveMode_ = CharacterMoveMode::None;
|
||||
runDirection_ = KeyboardMoveDirection::None;
|
||||
return;
|
||||
}
|
||||
|
||||
if (runDirection_ != KeyboardMoveDirection::None && IsDirectionPressed(runDirection_)) {
|
||||
keyboardMoveMode_ = CharacterMoveMode::Run;
|
||||
return;
|
||||
}
|
||||
|
||||
runDirection_ = KeyboardMoveDirection::None;
|
||||
keyboardMoveMode_ = CharacterMoveMode::Walk;
|
||||
}
|
||||
|
||||
bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) {
|
||||
auto handleDirectionPress = [&](KeyboardMoveDirection direction,
|
||||
bool& pressedFlag,
|
||||
std::uint32_t& lastTapTimestamp) {
|
||||
pressedFlag = true;
|
||||
std::uint32_t now = event.getTimestamp();
|
||||
bool isDoubleTap = lastTapTimestamp > 0
|
||||
&& now >= lastTapTimestamp
|
||||
&& now - lastTapTimestamp <= kKeyboardRunDoubleTapWindowMs;
|
||||
lastTapTimestamp = now;
|
||||
|
||||
if (isDoubleTap) {
|
||||
keyboardMoveMode_ = CharacterMoveMode::Run;
|
||||
runDirection_ = direction;
|
||||
} else if (keyboardMoveMode_ != CharacterMoveMode::Run
|
||||
|| !IsDirectionPressed(runDirection_)) {
|
||||
keyboardMoveMode_ = CharacterMoveMode::Walk;
|
||||
runDirection_ = KeyboardMoveDirection::None;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyCode::a:
|
||||
case KeyCode::Left:
|
||||
left_ = true;
|
||||
return true;
|
||||
return handleDirectionPress(
|
||||
KeyboardMoveDirection::Left, left_, leftLastTapTimestamp_);
|
||||
case KeyCode::d:
|
||||
case KeyCode::Right:
|
||||
right_ = true;
|
||||
return true;
|
||||
return handleDirectionPress(
|
||||
KeyboardMoveDirection::Right, right_, rightLastTapTimestamp_);
|
||||
case KeyCode::w:
|
||||
case KeyCode::Up:
|
||||
up_ = true;
|
||||
return true;
|
||||
return handleDirectionPress(
|
||||
KeyboardMoveDirection::Up, up_, upLastTapTimestamp_);
|
||||
case KeyCode::s:
|
||||
case KeyCode::Down:
|
||||
down_ = true;
|
||||
return handleDirectionPress(
|
||||
KeyboardMoveDirection::Down, down_, downLastTapTimestamp_);
|
||||
case KeyCode::x:
|
||||
QueueAction("attack");
|
||||
return true;
|
||||
case KeyCode::j:
|
||||
attackPressed_ = true;
|
||||
return true;
|
||||
case KeyCode::k:
|
||||
case KeyCode::c:
|
||||
jumpPressed_ = true;
|
||||
return true;
|
||||
case KeyCode::l:
|
||||
skill1Pressed_ = true;
|
||||
case KeyCode::z:
|
||||
QueueAction("skill_1");
|
||||
return true;
|
||||
case KeyCode::LShift:
|
||||
dashPressed_ = true;
|
||||
QueueAction("dash");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -47,23 +117,25 @@ bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) {
|
||||
}
|
||||
|
||||
bool CharacterInputRouter::OnKeyUp(const KeyUpEvent& event) {
|
||||
auto handleDirectionRelease = [&](bool& pressedFlag) {
|
||||
pressedFlag = false;
|
||||
RefreshKeyboardMoveMode();
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyCode::a:
|
||||
case KeyCode::Left:
|
||||
left_ = false;
|
||||
return true;
|
||||
return handleDirectionRelease(left_);
|
||||
case KeyCode::d:
|
||||
case KeyCode::Right:
|
||||
right_ = false;
|
||||
return true;
|
||||
return handleDirectionRelease(right_);
|
||||
case KeyCode::w:
|
||||
case KeyCode::Up:
|
||||
up_ = false;
|
||||
return true;
|
||||
return handleDirectionRelease(up_);
|
||||
case KeyCode::s:
|
||||
case KeyCode::Down:
|
||||
down_ = false;
|
||||
return true;
|
||||
return handleDirectionRelease(down_);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -88,14 +160,14 @@ bool CharacterInputRouter::OnJoystickButtonDown(
|
||||
jumpPressed_ = true;
|
||||
return true;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
attackPressed_ = true;
|
||||
QueueAction("attack");
|
||||
return true;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
|
||||
skill1Pressed_ = true;
|
||||
QueueAction("skill_1");
|
||||
return true;
|
||||
case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
|
||||
dashPressed_ = true;
|
||||
QueueAction("dash");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -110,23 +182,41 @@ bool CharacterInputRouter::OnJoystickButtonUp(
|
||||
|
||||
void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) {
|
||||
Vec2 moveAxis(leftStickX_, leftStickY_);
|
||||
if (moveAxis.lengthSquared() < 0.0001f) {
|
||||
CharacterMoveMode moveMode = CharacterMoveMode::None;
|
||||
float stickMagnitudeSquared = moveAxis.lengthSquared();
|
||||
if (stickMagnitudeSquared >= joystickDeadZone_ * joystickDeadZone_) {
|
||||
float stickMagnitude = std::sqrt(stickMagnitudeSquared);
|
||||
if (stickMagnitude > 1.0f) {
|
||||
moveAxis = moveAxis.normalized();
|
||||
stickMagnitude = 1.0f;
|
||||
}
|
||||
moveMode = stickMagnitude >= kAnalogRunThreshold
|
||||
? CharacterMoveMode::Run
|
||||
: CharacterMoveMode::Walk;
|
||||
} else {
|
||||
moveAxis.x = (right_ ? 1.0f : 0.0f) - (left_ ? 1.0f : 0.0f);
|
||||
moveAxis.y = (down_ ? 1.0f : 0.0f) - (up_ ? 1.0f : 0.0f);
|
||||
if (moveAxis.lengthSquared() > 1.0f) {
|
||||
moveAxis = moveAxis.normalized();
|
||||
}
|
||||
if (moveAxis.lengthSquared() >= 0.0001f) {
|
||||
moveMode = keyboardMoveMode_ == CharacterMoveMode::Run
|
||||
? CharacterMoveMode::Run
|
||||
: CharacterMoveMode::Walk;
|
||||
}
|
||||
}
|
||||
commandBuffer.Submit({CharacterCommandType::Move, moveAxis, 0.0f});
|
||||
commandBuffer.Submit(
|
||||
{CharacterCommandType::Move, moveAxis, 0.0f, moveMode, std::string()});
|
||||
|
||||
if (jumpPressed_) {
|
||||
commandBuffer.Submit({CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f});
|
||||
commandBuffer.Submit(
|
||||
{CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f,
|
||||
CharacterMoveMode::None, std::string()});
|
||||
}
|
||||
if (attackPressed_) {
|
||||
commandBuffer.Submit({CharacterCommandType::AttackPressed, Vec2::Zero(), 0.0f});
|
||||
}
|
||||
if (skill1Pressed_) {
|
||||
commandBuffer.Submit({CharacterCommandType::Skill1Pressed, Vec2::Zero(), 0.0f});
|
||||
}
|
||||
if (dashPressed_) {
|
||||
commandBuffer.Submit({CharacterCommandType::DashPressed, Vec2::Zero(), 0.0f});
|
||||
for (const auto& actionId : actionRequests_) {
|
||||
commandBuffer.Submit(
|
||||
{CharacterCommandType::ActionPressed, Vec2::Zero(), 0.0f,
|
||||
CharacterMoveMode::None, actionId});
|
||||
}
|
||||
|
||||
ClearFrameState();
|
||||
@@ -134,9 +224,7 @@ void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) {
|
||||
|
||||
void CharacterInputRouter::ClearFrameState() {
|
||||
jumpPressed_ = false;
|
||||
attackPressed_ = false;
|
||||
skill1Pressed_ = false;
|
||||
dashPressed_ = false;
|
||||
actionRequests_.clear();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/core/application.h>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
@@ -45,6 +46,9 @@ bool CharacterObject::Construction(int jobId) {
|
||||
actionLibrary_ = CharacterActionLibrary();
|
||||
commandBuffer_.Clear();
|
||||
currentIntent_ = CharacterIntent();
|
||||
runtimeBlackboard_.Clear();
|
||||
pendingActionContext_.Clear();
|
||||
currentActionContext_.Clear();
|
||||
stateMachine_.Reset();
|
||||
motor_ = CharacterMotor();
|
||||
status_ = CharacterStatus();
|
||||
@@ -76,12 +80,10 @@ bool CharacterObject::Construction(int jobId) {
|
||||
}
|
||||
AddChild(animationManager_);
|
||||
|
||||
const CharacterActionDefinition* idleAction = RequireAction(
|
||||
"idle", "CharacterObject::Construction");
|
||||
if (!idleAction) {
|
||||
if (!RequireAction("idle", "CharacterObject::Construction")) {
|
||||
return false;
|
||||
}
|
||||
if (!PlayActionDefinition(*idleAction, "CharacterObject::Construction")) {
|
||||
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -144,6 +146,68 @@ void CharacterObject::PushCommand(const CharacterCommand& command) {
|
||||
commandBuffer_.Submit(command);
|
||||
}
|
||||
|
||||
CharacterActionContextData& CharacterObject::PreparePendingActionContext(
|
||||
const std::string& requestedActionId,
|
||||
const std::string& sourceActionId,
|
||||
CharacterStateId sourceStateId) {
|
||||
pendingActionContext_.Clear();
|
||||
pendingActionContext_.valid = true;
|
||||
pendingActionContext_.requestedActionId = requestedActionId;
|
||||
pendingActionContext_.sourceActionId = sourceActionId;
|
||||
pendingActionContext_.sourceStateId = sourceStateId;
|
||||
pendingActionContext_.hasSourceStateId =
|
||||
sourceStateId != CharacterStateId::Idle || !sourceActionId.empty();
|
||||
return pendingActionContext_;
|
||||
}
|
||||
|
||||
void CharacterObject::ClearPendingActionContext() {
|
||||
pendingActionContext_.Clear();
|
||||
}
|
||||
|
||||
const CharacterActionContextData* CharacterObject::GetPendingActionContext() const {
|
||||
return pendingActionContext_.valid ? &pendingActionContext_ : nullptr;
|
||||
}
|
||||
|
||||
CharacterActionContextData* CharacterObject::GetPendingActionContextMutable() {
|
||||
return pendingActionContext_.valid ? &pendingActionContext_ : nullptr;
|
||||
}
|
||||
|
||||
const CharacterActionContextData* CharacterObject::GetCurrentActionContext() const {
|
||||
return currentActionContext_.valid ? ¤tActionContext_ : nullptr;
|
||||
}
|
||||
|
||||
CharacterActionContextData* CharacterObject::GetCurrentActionContextMutable() {
|
||||
return currentActionContext_.valid ? ¤tActionContext_ : nullptr;
|
||||
}
|
||||
|
||||
void CharacterObject::ClearCurrentActionContext() {
|
||||
currentActionContext_.Clear();
|
||||
}
|
||||
|
||||
void CharacterObject::CommitPendingActionContext(
|
||||
const std::string& defaultRequestedActionId,
|
||||
const std::string& defaultSourceActionId,
|
||||
CharacterStateId defaultSourceStateId) {
|
||||
currentActionContext_.Clear();
|
||||
if (pendingActionContext_.valid) {
|
||||
currentActionContext_ = pendingActionContext_;
|
||||
} else {
|
||||
currentActionContext_.valid = true;
|
||||
}
|
||||
|
||||
if (currentActionContext_.requestedActionId.empty()) {
|
||||
currentActionContext_.requestedActionId = defaultRequestedActionId;
|
||||
}
|
||||
if (currentActionContext_.sourceActionId.empty()) {
|
||||
currentActionContext_.sourceActionId = defaultSourceActionId;
|
||||
}
|
||||
if (!currentActionContext_.hasSourceStateId) {
|
||||
currentActionContext_.SetSourceStateId(defaultSourceStateId);
|
||||
}
|
||||
|
||||
pendingActionContext_.Clear();
|
||||
}
|
||||
|
||||
void CharacterObject::ApplyHit(const HitContext& hit) {
|
||||
if (status_.invincible) {
|
||||
return;
|
||||
@@ -224,20 +288,48 @@ const CharacterActionDefinition* CharacterObject::RequireAction(
|
||||
return action;
|
||||
}
|
||||
|
||||
bool CharacterObject::PlayActionDefinition(const CharacterActionDefinition& action,
|
||||
const char* phase) {
|
||||
if (action.animationTag.empty()) {
|
||||
ReportFatalCharacterError(phase, "logical action has empty animationTag",
|
||||
action.actionId, action.animationTag);
|
||||
return false;
|
||||
}
|
||||
return SetActionStrict(action.animationTag, phase, action.actionId);
|
||||
}
|
||||
|
||||
void CharacterObject::PlayAnimationTag(const std::string& actionTag) {
|
||||
SetActionStrict(actionTag, "CharacterObject::PlayAnimationTag", std::string());
|
||||
}
|
||||
|
||||
bool CharacterObject::HasAnimationTag(const std::string& actionTag) const {
|
||||
return animationManager_ && animationManager_->HasAction(actionTag);
|
||||
}
|
||||
|
||||
bool CharacterObject::IsCurrentAnimationFinished() const {
|
||||
return animationManager_ && animationManager_->IsCurrentActionFinished();
|
||||
}
|
||||
|
||||
int CharacterObject::GetCurrentAnimationFrameIndex() const {
|
||||
return animationManager_ ? animationManager_->GetCurrentFrameIndex() : 0;
|
||||
}
|
||||
|
||||
int CharacterObject::GetCurrentAnimationFrameCount() const {
|
||||
return animationManager_ ? animationManager_->GetCurrentFrameCount() : 0;
|
||||
}
|
||||
|
||||
float CharacterObject::GetCurrentAnimationProgressNormalized() const {
|
||||
return animationManager_ ? animationManager_->GetCurrentNormalizedProgress() : 0.0f;
|
||||
}
|
||||
|
||||
animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const {
|
||||
return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame();
|
||||
}
|
||||
|
||||
void CharacterObject::SetAnimationFrameFlagCallback(
|
||||
CharacterAnimation::ActionFrameFlagCallback callback) {
|
||||
if (animationManager_) {
|
||||
animationManager_->SetActionFrameFlagCallback(std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterObject::SetAnimationEndCallback(
|
||||
CharacterAnimation::ActionEndCallback callback) {
|
||||
if (animationManager_) {
|
||||
animationManager_->SetActionEndCallback(std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
std::string CharacterObject::DescribeConfiguredAnimationTags() const {
|
||||
if (!config_ || config_->animationPath.empty()) {
|
||||
return "<none>";
|
||||
@@ -273,6 +365,30 @@ void CharacterObject::ReportFatalCharacterError(
|
||||
<< " currentActionId: "
|
||||
<< NonEmptyOrPlaceholder(stateMachine_.GetCurrentActionId(), "<none>") << '\n'
|
||||
<< " currentAnimationTag: " << NonEmptyOrPlaceholder(currentAction_, "<none>") << '\n'
|
||||
<< " currentActionContext.requestedActionId: "
|
||||
<< NonEmptyOrPlaceholder(
|
||||
currentActionContext_.valid ? currentActionContext_.requestedActionId
|
||||
: std::string(),
|
||||
"<none>")
|
||||
<< '\n'
|
||||
<< " currentActionContext.sourceActionId: "
|
||||
<< NonEmptyOrPlaceholder(
|
||||
currentActionContext_.valid ? currentActionContext_.sourceActionId
|
||||
: std::string(),
|
||||
"<none>")
|
||||
<< '\n'
|
||||
<< " pendingActionContext.requestedActionId: "
|
||||
<< NonEmptyOrPlaceholder(
|
||||
pendingActionContext_.valid ? pendingActionContext_.requestedActionId
|
||||
: std::string(),
|
||||
"<none>")
|
||||
<< '\n'
|
||||
<< " pendingActionContext.sourceActionId: "
|
||||
<< NonEmptyOrPlaceholder(
|
||||
pendingActionContext_.valid ? pendingActionContext_.sourceActionId
|
||||
: std::string(),
|
||||
"<none>")
|
||||
<< '\n'
|
||||
<< " requestedActionId: "
|
||||
<< NonEmptyOrPlaceholder(requestedActionId, "<none>") << '\n'
|
||||
<< " requestedAnimationTag: "
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
#include "character/states/CharacterStateRegistry.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
bool IsCompatiblePendingActionContext(const CharacterActionContextData* context,
|
||||
const std::string& requestedActionId) {
|
||||
return !context || context->requestedActionId.empty()
|
||||
|| context->requestedActionId == requestedActionId;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CharacterStateMachine::Configure(const character::CharacterConfig& config) {
|
||||
ClearRegisteredStates();
|
||||
@@ -16,9 +25,6 @@ void CharacterStateMachine::Reset() {
|
||||
currentActionId_.clear();
|
||||
currentAction_ = nullptr;
|
||||
stateTime_ = 0.0f;
|
||||
actionFrameProgress_ = 0.0f;
|
||||
actionFrame_ = 0;
|
||||
landingTimer_ = 0.0f;
|
||||
}
|
||||
|
||||
void CharacterStateMachine::Update(CharacterObject& owner,
|
||||
@@ -41,14 +47,14 @@ void CharacterStateMachine::ForceHurt(CharacterObject& owner,
|
||||
return;
|
||||
}
|
||||
|
||||
owner.ClearPendingActionContext();
|
||||
|
||||
const CharacterActionDefinition* previousAction = currentAction_;
|
||||
currentAction_ = hurtAction;
|
||||
currentActionId_ = hurtAction->actionId;
|
||||
if (currentState_ == CharacterStateId::Hurt) {
|
||||
InvokeStateHook(owner, previousAction, "on_exit");
|
||||
stateTime_ = 0.0f;
|
||||
actionFrameProgress_ = 0.0f;
|
||||
actionFrame_ = 0;
|
||||
CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
|
||||
owner.GetLastDeltaTime()};
|
||||
if (auto* hurtNode = FindStateNode(CharacterStateId::Hurt)) {
|
||||
@@ -66,20 +72,19 @@ bool CharacterStateMachine::CanAcceptGroundActions() const {
|
||||
}
|
||||
|
||||
bool CharacterStateMachine::CanBeInterrupted() const {
|
||||
if (currentState_ == CharacterStateId::Action && currentAction_) {
|
||||
return currentAction_->canBeInterrupted;
|
||||
}
|
||||
return currentState_ != CharacterStateId::Dead;
|
||||
}
|
||||
|
||||
bool CharacterStateMachine::IsMovementLocked() const {
|
||||
return currentState_ == CharacterStateId::Action && currentAction_
|
||||
&& !currentAction_->canMove;
|
||||
return currentState_ == CharacterStateId::Action
|
||||
|| currentState_ == CharacterStateId::Hurt
|
||||
|| currentState_ == CharacterStateId::Dead;
|
||||
}
|
||||
|
||||
bool CharacterStateMachine::CanTurn() const {
|
||||
return !(currentState_ == CharacterStateId::Action && currentAction_
|
||||
&& !currentAction_->canTurn);
|
||||
return currentState_ != CharacterStateId::Action
|
||||
&& currentState_ != CharacterStateId::Hurt
|
||||
&& currentState_ != CharacterStateId::Dead;
|
||||
}
|
||||
|
||||
void CharacterStateMachine::RegisterState(std::unique_ptr<ICharacterStateNode> stateNode) {
|
||||
@@ -115,6 +120,13 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner,
|
||||
previousNode->OnExit(*this, context, nextState);
|
||||
}
|
||||
|
||||
if (previousState == CharacterStateId::Action && nextState != CharacterStateId::Action) {
|
||||
owner.ClearCurrentActionContext();
|
||||
}
|
||||
if (nextState != CharacterStateId::Action) {
|
||||
owner.ClearPendingActionContext();
|
||||
}
|
||||
|
||||
currentState_ = nextState;
|
||||
stateTime_ = 0.0f;
|
||||
if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) {
|
||||
@@ -135,21 +147,28 @@ void CharacterStateMachine::EnterAction(CharacterObject& owner,
|
||||
return;
|
||||
}
|
||||
|
||||
const CharacterActionDefinition* previousAction = currentAction_;
|
||||
currentAction_ = action;
|
||||
currentActionId_ = action->actionId;
|
||||
const std::string previousActionId = currentActionId_;
|
||||
CharacterStateId previousState = currentState_;
|
||||
if (currentState_ == CharacterStateId::Action) {
|
||||
InvokeStateHook(owner, previousAction, "on_exit");
|
||||
stateTime_ = 0.0f;
|
||||
actionFrameProgress_ = 0.0f;
|
||||
actionFrame_ = 0;
|
||||
CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
|
||||
owner.GetLastDeltaTime()};
|
||||
if (auto* actionNode = FindStateNode(CharacterStateId::Action)) {
|
||||
actionNode->OnExit(*this, context, CharacterStateId::Action);
|
||||
}
|
||||
|
||||
currentAction_ = action;
|
||||
currentActionId_ = action->actionId;
|
||||
owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState);
|
||||
stateTime_ = 0.0f;
|
||||
if (auto* actionNode = FindStateNode(CharacterStateId::Action)) {
|
||||
actionNode->OnEnter(*this, context, CharacterStateId::Action);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentAction_ = action;
|
||||
currentActionId_ = action->actionId;
|
||||
owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState);
|
||||
ChangeState(owner, CharacterStateId::Action);
|
||||
}
|
||||
|
||||
@@ -164,16 +183,38 @@ void CharacterStateMachine::InvokeStateHook(CharacterObject& owner,
|
||||
|
||||
bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
CharacterCommandType commandType,
|
||||
const std::string& requestedActionId,
|
||||
const std::string& actionId) {
|
||||
if (!commandBuffer.HasBuffered(commandType)) {
|
||||
if (!commandBuffer.HasBufferedAction(requestedActionId)) {
|
||||
if (IsCompatiblePendingActionContext(owner.GetPendingActionContext(), requestedActionId)) {
|
||||
owner.ClearPendingActionContext();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
commandBuffer.ConsumeAction(requestedActionId);
|
||||
return StartAction(owner, requestedActionId, actionId);
|
||||
}
|
||||
|
||||
bool CharacterStateMachine::StartAction(CharacterObject& owner,
|
||||
const std::string& requestedActionId,
|
||||
const std::string& actionId) {
|
||||
const CharacterActionContextData* pendingActionContext = owner.GetPendingActionContext();
|
||||
if (!IsCompatiblePendingActionContext(pendingActionContext, requestedActionId)) {
|
||||
owner.ReportFatalCharacterError("CharacterStateMachine::StartAction",
|
||||
"prepared action context does not match requested action",
|
||||
requestedActionId);
|
||||
owner.ClearPendingActionContext();
|
||||
return false;
|
||||
}
|
||||
if (!pendingActionContext) {
|
||||
owner.PreparePendingActionContext(requestedActionId, currentActionId_, currentState_);
|
||||
}
|
||||
|
||||
const CharacterActionDefinition* action = owner.RequireAction(
|
||||
actionId, "CharacterStateMachine::TryStartAction");
|
||||
commandBuffer.Consume(commandType);
|
||||
actionId, "CharacterStateMachine::StartAction");
|
||||
if (!action) {
|
||||
owner.ClearPendingActionContext();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -190,34 +231,4 @@ bool CharacterStateMachine::TryStartRegisteredAction(CharacterStateContext& cont
|
||||
return actionNode->TryEnter(*this, context);
|
||||
}
|
||||
|
||||
void CharacterStateMachine::ApplyFrameEvents(CharacterObject& owner,
|
||||
int previousFrame,
|
||||
int currentFrame) {
|
||||
if (!currentAction_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& motor = owner.GetMotorMutable();
|
||||
for (const auto& event : currentAction_->frameEvents) {
|
||||
if (event.frame < previousFrame || event.frame > currentFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case CharacterFrameEventType::SetVelocityXY:
|
||||
motor.groundVelocity = event.velocityXY;
|
||||
break;
|
||||
case CharacterFrameEventType::SetVelocityZ:
|
||||
motor.verticalVelocity = event.velocityZ;
|
||||
motor.grounded = false;
|
||||
break;
|
||||
case CharacterFrameEventType::FinishAction:
|
||||
actionFrame_ = currentAction_->totalFrames;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -18,15 +18,6 @@ void CharacterStateBase::ChangeState(CharacterStateMachine& machine,
|
||||
machine.ChangeState(context.owner, nextState);
|
||||
}
|
||||
|
||||
void CharacterStateBase::PlayActionById(CharacterObject& owner,
|
||||
const std::string& actionId) const {
|
||||
const auto* action = owner.RequireAction(actionId, "CharacterStateBase::PlayActionById");
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
owner.PlayActionDefinition(*action, "CharacterStateBase::PlayActionById");
|
||||
}
|
||||
|
||||
void CharacterStateBase::InvokeHook(CharacterStateMachine& machine,
|
||||
CharacterObject& owner,
|
||||
const CharacterActionDefinition* action,
|
||||
@@ -36,9 +27,29 @@ void CharacterStateBase::InvokeHook(CharacterStateMachine& machine,
|
||||
|
||||
bool CharacterStateBase::TryStartAction(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterCommandType commandType,
|
||||
const std::string& actionId) const {
|
||||
return machine.TryStartAction(context.owner, context.commandBuffer, commandType, actionId);
|
||||
return machine.TryStartAction(context.owner, context.commandBuffer, actionId, actionId);
|
||||
}
|
||||
|
||||
bool CharacterStateBase::TryStartAction(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
const std::string& requestedActionId,
|
||||
const std::string& actionId) const {
|
||||
return machine.TryStartAction(
|
||||
context.owner, context.commandBuffer, requestedActionId, actionId);
|
||||
}
|
||||
|
||||
bool CharacterStateBase::StartAction(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
const std::string& actionId) const {
|
||||
return machine.StartAction(context.owner, actionId, actionId);
|
||||
}
|
||||
|
||||
bool CharacterStateBase::StartAction(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
const std::string& requestedActionId,
|
||||
const std::string& actionId) const {
|
||||
return machine.StartAction(context.owner, requestedActionId, actionId);
|
||||
}
|
||||
|
||||
bool CharacterStateBase::TryStartRegisteredAction(CharacterStateMachine& machine,
|
||||
@@ -46,13 +57,6 @@ bool CharacterStateBase::TryStartRegisteredAction(CharacterStateMachine& machine
|
||||
return machine.TryStartRegisteredAction(context);
|
||||
}
|
||||
|
||||
void CharacterStateBase::ApplyFrameEvents(CharacterStateMachine& machine,
|
||||
CharacterObject& owner,
|
||||
int previousFrame,
|
||||
int currentFrame) const {
|
||||
machine.ApplyFrameEvents(owner, previousFrame, currentFrame);
|
||||
}
|
||||
|
||||
const CharacterActionDefinition* CharacterStateBase::GetCurrentAction(
|
||||
const CharacterStateMachine& machine) const {
|
||||
return machine.currentAction_;
|
||||
@@ -79,16 +83,4 @@ float& CharacterStateBase::StateTime(CharacterStateMachine& machine) const {
|
||||
return machine.stateTime_;
|
||||
}
|
||||
|
||||
float& CharacterStateBase::ActionFrameProgress(CharacterStateMachine& machine) const {
|
||||
return machine.actionFrameProgress_;
|
||||
}
|
||||
|
||||
int& CharacterStateBase::ActionFrame(CharacterStateMachine& machine) const {
|
||||
return machine.actionFrame_;
|
||||
}
|
||||
|
||||
float& CharacterStateBase::LandingTimer(CharacterStateMachine& machine) const {
|
||||
return machine.landingTimer_;
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -11,7 +11,7 @@ void FallState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
PlayActionById(context.owner, "fall");
|
||||
context.owner.PlayAnimationTag("fall");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("fall"), "on_enter");
|
||||
}
|
||||
|
||||
@@ -32,12 +32,7 @@ void FallState::OnUpdate(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
|
||||
context.commandBuffer.Consume(CharacterCommandType::AttackPressed);
|
||||
}
|
||||
if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
|
||||
context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
|
||||
}
|
||||
context.commandBuffer.ClearBufferedActions();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -26,8 +26,6 @@ void HurtState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
ActionFrameProgress(machine) = 0.0f;
|
||||
ActionFrame(machine) = 0;
|
||||
const CharacterActionDefinition* action = GetCurrentAction(machine);
|
||||
if (!action) {
|
||||
context.owner.ReportFatalCharacterError("HurtState::OnEnter",
|
||||
@@ -35,9 +33,7 @@ void HurtState::OnEnter(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.owner.PlayActionDefinition(*action, "HurtState::OnEnter")) {
|
||||
return;
|
||||
}
|
||||
context.owner.PlayAnimationTag(action->actionId);
|
||||
InvokeHook(machine, context.owner, action, "on_enter");
|
||||
}
|
||||
|
||||
@@ -56,9 +52,7 @@ void HurtState::OnUpdate(CharacterStateMachine& machine,
|
||||
|
||||
InvokeHook(machine, owner, action, "on_update");
|
||||
|
||||
ActionFrameProgress(machine) += context.deltaTime * 60.0f;
|
||||
ActionFrame(machine) = std::max(0, static_cast<int>(ActionFrameProgress(machine)));
|
||||
if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) {
|
||||
if (owner.IsCurrentAnimationFinished()) {
|
||||
ClearCurrentAction(machine);
|
||||
context.owner.SetFacing(motor.facing);
|
||||
ChangeState(machine, context, ResolveInterruptedState(context));
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
bool HasMoveIntent(const CharacterIntent& intent) {
|
||||
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
|
||||
return intent.moveMode != CharacterMoveMode::None
|
||||
&& (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -19,7 +20,7 @@ void IdleState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
PlayActionById(context.owner, "idle");
|
||||
context.owner.PlayAnimationTag("rest");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("idle"), "on_enter");
|
||||
}
|
||||
|
||||
@@ -43,8 +44,6 @@ void IdleState::OnUpdate(CharacterStateMachine& machine,
|
||||
}
|
||||
|
||||
if (HasMoveIntent(context.intent)) {
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
|
||||
owner.SetFacing(motor.facing);
|
||||
ChangeState(machine, context, CharacterStateId::Move);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ void JumpState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
PlayActionById(context.owner, "jump");
|
||||
context.owner.PlayAnimationTag("jump");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("jump"), "on_enter");
|
||||
}
|
||||
|
||||
@@ -32,12 +32,7 @@ void JumpState::OnUpdate(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
|
||||
context.commandBuffer.Consume(CharacterCommandType::AttackPressed);
|
||||
}
|
||||
if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
|
||||
context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
|
||||
}
|
||||
context.commandBuffer.ClearBufferedActions();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr float kLandingDuration = 0.08f;
|
||||
|
||||
bool HasMoveIntent(const CharacterIntent& intent) {
|
||||
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
|
||||
return intent.moveMode != CharacterMoveMode::None
|
||||
&& (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -21,8 +20,7 @@ void LandingState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
LandingTimer(machine) = 0.0f;
|
||||
PlayActionById(context.owner, "landing");
|
||||
context.owner.PlayAnimationTag("landing");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("landing"), "on_enter");
|
||||
}
|
||||
|
||||
@@ -35,9 +33,8 @@ void LandingState::OnUpdate(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
LandingTimer(machine) += context.deltaTime;
|
||||
motor.StopGroundMovement();
|
||||
if (LandingTimer(machine) < kLandingDuration) {
|
||||
if (!owner.IsCurrentAnimationFinished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,8 +49,6 @@ void LandingState::OnUpdate(CharacterStateMachine& machine,
|
||||
}
|
||||
|
||||
if (HasMoveIntent(context.intent)) {
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
|
||||
owner.SetFacing(motor.facing);
|
||||
ChangeState(machine, context, CharacterStateId::Move);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,33 @@
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr float kWalkMoveSpeedScale = 0.65f;
|
||||
constexpr float kRunMoveSpeedScale = 1.0f;
|
||||
|
||||
bool HasMoveIntent(const CharacterIntent& intent) {
|
||||
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
|
||||
return intent.moveMode != CharacterMoveMode::None
|
||||
&& (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f);
|
||||
}
|
||||
|
||||
float ResolveMoveSpeedScale(const CharacterIntent& intent) {
|
||||
return intent.moveMode == CharacterMoveMode::Run
|
||||
? kRunMoveSpeedScale
|
||||
: kWalkMoveSpeedScale;
|
||||
}
|
||||
|
||||
const char* ResolveMoveActionId(const CharacterIntent& intent) {
|
||||
return intent.moveMode == CharacterMoveMode::Run ? "run" : "move";
|
||||
}
|
||||
|
||||
const char* ResolveMoveAnimationTag(const CharacterIntent& intent) {
|
||||
return intent.moveMode == CharacterMoveMode::Run ? "run" : "move";
|
||||
}
|
||||
|
||||
void SyncMovePresentation(CharacterObject& owner, const CharacterIntent& intent) {
|
||||
const char* animationTag = ResolveMoveAnimationTag(intent);
|
||||
if (owner.GetCurrentAction() != animationTag) {
|
||||
owner.PlayAnimationTag(animationTag);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -19,8 +44,18 @@ void MoveState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
PlayActionById(context.owner, "move");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("move"), "on_enter");
|
||||
auto& owner = context.owner;
|
||||
auto& motor = owner.GetMotorMutable();
|
||||
if (!HasMoveIntent(context.intent)) {
|
||||
motor.StopGroundMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY,
|
||||
ResolveMoveSpeedScale(context.intent));
|
||||
owner.SetFacing(motor.facing);
|
||||
owner.PlayAnimationTag(ResolveMoveAnimationTag(context.intent));
|
||||
InvokeHook(machine, owner, owner.FindAction(ResolveMoveActionId(context.intent)), "on_enter");
|
||||
}
|
||||
|
||||
void MoveState::OnUpdate(CharacterStateMachine& machine,
|
||||
@@ -42,12 +77,16 @@ void MoveState::OnUpdate(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
|
||||
owner.SetFacing(motor.facing);
|
||||
if (!HasMoveIntent(context.intent)) {
|
||||
motor.StopGroundMovement();
|
||||
ChangeState(machine, context, CharacterStateId::Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY,
|
||||
ResolveMoveSpeedScale(context.intent));
|
||||
owner.SetFacing(motor.facing);
|
||||
SyncMovePresentation(owner, context.intent);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "character/states/jobs/swordman/SwordmanActionHandler.h"
|
||||
#include <utility>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
SwordmanActionHandlerBase::SwordmanActionHandlerBase(std::string actionId)
|
||||
: actionId_(std::move(actionId)) {
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,16 +1,16 @@
|
||||
#include "character/states/jobs/swordman/SwordmanAttackState.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include <algorithm>
|
||||
#include "character/states/jobs/swordman/SwordmanActionHandler.h"
|
||||
#include "character/states/jobs/swordman/SwordmanBasicAttackHandler.h"
|
||||
#include "character/states/jobs/swordman/SwordmanSkill1Handler.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr float kLogicFramesPerSecond = 60.0f;
|
||||
constexpr float kMovementActionScale = 0.35f;
|
||||
|
||||
bool HasMoveIntent(const CharacterIntent& intent) {
|
||||
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
|
||||
return intent.moveMode != CharacterMoveMode::None
|
||||
&& (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f);
|
||||
}
|
||||
|
||||
CharacterStateId ResolveGroundState(const CharacterIntent& intent) {
|
||||
@@ -21,22 +21,43 @@ CharacterStateId ResolveGroundState(const CharacterIntent& intent) {
|
||||
|
||||
SwordmanAttackState::SwordmanAttackState()
|
||||
: CharacterStateBase(CharacterStateId::Action) {
|
||||
handlers_.emplace("attack", std::make_unique<SwordmanBasicAttackHandler>());
|
||||
handlers_.emplace("skill_1", std::make_unique<SwordmanSkill1Handler>());
|
||||
}
|
||||
|
||||
bool SwordmanAttackState::TryEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context) {
|
||||
if (TryStartAction(machine, context, CharacterCommandType::Skill1Pressed, "skill_1")) {
|
||||
return true;
|
||||
for (const auto& requestedActionId : context.intent.requestedActions) {
|
||||
const CharacterActionDefinition* action = context.owner.FindAction(requestedActionId);
|
||||
if (!action) {
|
||||
context.owner.ReportFatalCharacterError(
|
||||
"SwordmanAttackState::TryEnter",
|
||||
"requested logical action is not registered",
|
||||
requestedActionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FindHandler(action->actionId)) {
|
||||
context.owner.ReportFatalCharacterError(
|
||||
"SwordmanAttackState::TryEnter",
|
||||
"requested logical action has no swordman action handler",
|
||||
action->actionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryStartAction(machine, context, requestedActionId, action->actionId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return TryStartAction(machine, context, CharacterCommandType::AttackPressed, "attack_1");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SwordmanAttackState::OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) {
|
||||
(void)previousState;
|
||||
ActionFrameProgress(machine) = 0.0f;
|
||||
ActionFrame(machine) = 0;
|
||||
|
||||
const CharacterActionDefinition* action = GetCurrentAction(machine);
|
||||
if (!action) {
|
||||
context.owner.ReportFatalCharacterError("SwordmanAttackState::OnEnter",
|
||||
@@ -44,9 +65,15 @@ void SwordmanAttackState::OnEnter(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.owner.PlayActionDefinition(*action, "SwordmanAttackState::OnEnter")) {
|
||||
ISwordmanActionHandler* handler = FindHandler(action->actionId);
|
||||
if (!handler) {
|
||||
context.owner.ReportFatalCharacterError("SwordmanAttackState::OnEnter",
|
||||
"current logical action has no swordman handler",
|
||||
action->actionId);
|
||||
return;
|
||||
}
|
||||
|
||||
handler->OnEnter(context, *action);
|
||||
InvokeHook(machine, context.owner, action, "on_enter");
|
||||
}
|
||||
|
||||
@@ -60,44 +87,32 @@ void SwordmanAttackState::OnUpdate(CharacterStateMachine& machine,
|
||||
return;
|
||||
}
|
||||
|
||||
ISwordmanActionHandler* handler = FindHandler(action->actionId);
|
||||
if (!handler) {
|
||||
owner.ReportFatalCharacterError("SwordmanAttackState::OnUpdate",
|
||||
"current logical action has no swordman handler",
|
||||
action->actionId);
|
||||
return;
|
||||
}
|
||||
|
||||
InvokeHook(machine, owner, action, "on_update");
|
||||
SwordmanActionUpdateResult updateResult = handler->OnUpdate(context, *action);
|
||||
if (updateResult.type == SwordmanActionUpdateResult::Type::Transition) {
|
||||
if (updateResult.requestedActionId.empty()) {
|
||||
owner.ReportFatalCharacterError("SwordmanAttackState::OnUpdate",
|
||||
"handler requested an empty transition action id",
|
||||
action->actionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action->canMove) {
|
||||
float moveScale = action->moveSpeedScale > 0.0f ? action->moveSpeedScale : kMovementActionScale;
|
||||
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, moveScale);
|
||||
} else {
|
||||
motor.StopGroundMovement();
|
||||
}
|
||||
if (action->canTurn) {
|
||||
owner.SetFacing(motor.facing);
|
||||
bool started = updateResult.requireBufferedInput
|
||||
? TryStartAction(machine, context, updateResult.requestedActionId)
|
||||
: StartAction(machine, context, updateResult.requestedActionId);
|
||||
(void)started;
|
||||
return;
|
||||
}
|
||||
|
||||
ActionFrameProgress(machine) += context.deltaTime * kLogicFramesPerSecond;
|
||||
int previousFrame = ActionFrame(machine);
|
||||
ActionFrame(machine) = std::max(0, static_cast<int>(ActionFrameProgress(machine)));
|
||||
ApplyFrameEvents(machine, owner, previousFrame + 1, ActionFrame(machine));
|
||||
|
||||
for (const auto& rule : action->cancelRules) {
|
||||
if (ActionFrame(machine) < rule.beginFrame || ActionFrame(machine) > rule.endFrame) {
|
||||
continue;
|
||||
}
|
||||
if (rule.requireGrounded && !motor.grounded) {
|
||||
continue;
|
||||
}
|
||||
if (rule.requireAirborne && motor.grounded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.targetAction == "attack_2"
|
||||
&& context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
|
||||
if (TryStartAction(machine, context, CharacterCommandType::AttackPressed, rule.targetAction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) {
|
||||
ClearCurrentAction(machine);
|
||||
if (updateResult.type == SwordmanActionUpdateResult::Type::Finished) {
|
||||
if (!motor.grounded) {
|
||||
ChangeState(machine, context, CharacterStateId::Fall);
|
||||
} else {
|
||||
@@ -110,7 +125,22 @@ void SwordmanAttackState::OnExit(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId nextState) {
|
||||
(void)nextState;
|
||||
InvokeHook(machine, context.owner, GetCurrentAction(machine), "on_exit");
|
||||
|
||||
const CharacterActionDefinition* action = GetCurrentAction(machine);
|
||||
if (action) {
|
||||
if (ISwordmanActionHandler* handler = FindHandler(action->actionId)) {
|
||||
handler->OnExit(context, *action);
|
||||
}
|
||||
}
|
||||
InvokeHook(machine, context.owner, action, "on_exit");
|
||||
}
|
||||
|
||||
ISwordmanActionHandler* SwordmanAttackState::FindHandler(const std::string& actionId) const {
|
||||
auto it = handlers_.find(actionId);
|
||||
if (it == handlers_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "character/states/jobs/swordman/SwordmanBasicAttackHandler.h"
|
||||
#include "character/CharacterObject.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr float kAttackChainWindowBegin = 0.35f;
|
||||
constexpr float kAttackChainWindowEnd = 0.95f;
|
||||
constexpr float kDirectionalIntentThreshold = 0.25f;
|
||||
|
||||
struct AttackSlideConfig {
|
||||
float neutralDistance = 0.0f;
|
||||
float forwardDistance = 0.0f;
|
||||
float duration = 0.0f;
|
||||
};
|
||||
|
||||
constexpr AttackSlideConfig kAttackSlideConfigs[] = {
|
||||
{0.0f, 0.0f, 0.0f},
|
||||
{16.0f, 28.0f, 0.08f},
|
||||
{20.0f, 34.0f, 0.09f},
|
||||
{24.0f, 40.0f, 0.10f},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SwordmanBasicAttackHandler::SwordmanBasicAttackHandler()
|
||||
: SwordmanActionHandlerBase("attack") {
|
||||
}
|
||||
|
||||
void SwordmanBasicAttackHandler::OnEnter(CharacterStateContext& context,
|
||||
const CharacterActionDefinition& action) {
|
||||
(void)action;
|
||||
BeginComboStep(context, 1);
|
||||
}
|
||||
|
||||
SwordmanActionUpdateResult SwordmanBasicAttackHandler::OnUpdate(
|
||||
CharacterStateContext& context,
|
||||
const CharacterActionDefinition& action) {
|
||||
(void)action;
|
||||
|
||||
context.owner.GetMotorMutable().StopGroundMovement();
|
||||
if (comboStep_ == 1) {
|
||||
float progress = context.owner.GetCurrentAnimationProgressNormalized();
|
||||
if (progress >= kAttackChainWindowBegin
|
||||
&& progress <= kAttackChainWindowEnd
|
||||
&& context.commandBuffer.HasBufferedAction(GetActionId())) {
|
||||
context.commandBuffer.ConsumeAction(GetActionId());
|
||||
BeginComboStep(context, 2);
|
||||
return SwordmanActionUpdateResult::Running();
|
||||
}
|
||||
}
|
||||
|
||||
if (comboStep_ == 2) {
|
||||
float progress = context.owner.GetCurrentAnimationProgressNormalized();
|
||||
if (progress >= kAttackChainWindowBegin
|
||||
&& progress <= kAttackChainWindowEnd
|
||||
&& context.commandBuffer.HasBufferedAction(GetActionId())) {
|
||||
context.commandBuffer.ConsumeAction(GetActionId());
|
||||
BeginComboStep(context, 3);
|
||||
return SwordmanActionUpdateResult::Running();
|
||||
}
|
||||
}
|
||||
|
||||
return context.owner.IsCurrentAnimationFinished()
|
||||
? SwordmanActionUpdateResult::Finished()
|
||||
: SwordmanActionUpdateResult::Running();
|
||||
}
|
||||
|
||||
void SwordmanBasicAttackHandler::OnExit(CharacterStateContext& context,
|
||||
const CharacterActionDefinition& action) {
|
||||
(void)context;
|
||||
(void)action;
|
||||
comboStep_ = 0;
|
||||
}
|
||||
|
||||
void SwordmanBasicAttackHandler::BeginComboStep(CharacterStateContext& context, int comboStep) {
|
||||
comboStep_ = comboStep;
|
||||
|
||||
auto& owner = context.owner;
|
||||
auto& motor = owner.GetMotorMutable();
|
||||
motor.StopGroundMovement();
|
||||
|
||||
switch (comboStep_) {
|
||||
case 1:
|
||||
owner.PlayAnimationTag("attack1");
|
||||
break;
|
||||
case 2:
|
||||
owner.PlayAnimationTag("attack2");
|
||||
break;
|
||||
case 3:
|
||||
owner.PlayAnimationTag("attack3");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const AttackSlideConfig& slideConfig = kAttackSlideConfigs[comboStep_];
|
||||
float forwardInputStrength = context.intent.moveX * static_cast<float>(motor.facing);
|
||||
float slideDistance = slideConfig.neutralDistance;
|
||||
if (forwardInputStrength >= kDirectionalIntentThreshold) {
|
||||
slideDistance = slideConfig.forwardDistance;
|
||||
} else if (forwardInputStrength <= -kDirectionalIntentThreshold) {
|
||||
slideDistance = 0.0f;
|
||||
}
|
||||
|
||||
motor.StartForcedSlide(Vec2(static_cast<float>(motor.facing), 0.0f),
|
||||
slideDistance, slideConfig.duration);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "character/states/jobs/swordman/SwordmanSkill1Handler.h"
|
||||
#include "SDL_log.h"
|
||||
#include "character/CharacterObject.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
SwordmanSkill1Handler::SwordmanSkill1Handler()
|
||||
: SwordmanActionHandlerBase("skill_1") {
|
||||
}
|
||||
|
||||
void SwordmanSkill1Handler::OnEnter(CharacterStateContext& context,
|
||||
const CharacterActionDefinition& action) {
|
||||
(void)action;
|
||||
context.owner.GetMotorMutable().StopGroundMovement();
|
||||
context.owner.PlayAnimationTag("Guard");
|
||||
}
|
||||
|
||||
SwordmanActionUpdateResult SwordmanSkill1Handler::OnUpdate(
|
||||
CharacterStateContext& context,
|
||||
const CharacterActionDefinition& action) {
|
||||
(void)action;
|
||||
|
||||
context.owner.GetMotorMutable().StopGroundMovement();
|
||||
|
||||
|
||||
return context.owner.IsCurrentAnimationFinished()
|
||||
? SwordmanActionUpdateResult::Finished()
|
||||
: SwordmanActionUpdateResult::Running();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
Reference in New Issue
Block a user