feat(npc): 添加NPC数据加载、动画和对象功能

实现NPC系统的核心功能,包括:
1. 新增NpcDataLoader用于加载NPC索引和配置数据
2. 添加NpcAnimation处理NPC动画显示
3. 创建NpcObject实现NPC交互和显示逻辑
4. 在GameMapTestScene中集成测试NPC功能
This commit is contained in:
2026-04-07 23:02:03 +08:00
parent 6855860d64
commit caad22cca7
9 changed files with 628 additions and 7 deletions

214
Game/src/npc/NpcObject.cpp Normal file
View File

@@ -0,0 +1,214 @@
#include "npc/NpcObject.h"
#include "common/math/GameMath.h"
#include <SDL2/SDL.h>
namespace frostbite2D {
namespace {
constexpr float kNpcNameMarginY = 8.0f;
constexpr float kNpcNameFallbackTopY = -96.0f;
const Vec2 kNpcNameOutlineOffsets[] = {
Vec2(-1.0f, -1.0f), Vec2(-1.0f, 0.0f), Vec2(-1.0f, 1.0f),
Vec2(0.0f, -1.0f), Vec2(0.0f, 1.0f),
Vec2(1.0f, -1.0f), Vec2(1.0f, 0.0f), Vec2(1.0f, 1.0f),
};
void ConfigureNameTextLabel(RefPtr<TextSprite> label,
const char* name,
const Color& color,
int zOrder) {
if (!label) {
return;
}
label->SetName(name);
label->SetFont("default");
label->SetAnchor(0.5f, 1.0f);
label->SetTextColor(color);
label->SetZOrder(zOrder);
}
const std::string& EmptyString() {
static const std::string kEmpty;
return kEmpty;
}
} // namespace
bool NpcObject::Construction(int npcId) {
RemoveAllChildren();
npcId_ = -1;
direction_ = 1;
worldPosition_ = CharacterWorldPosition();
config_.reset();
animation_ = nullptr;
nameOutlineLabels_.fill(nullptr);
nameLabel_ = nullptr;
interactable_ = true;
interacting_ = false;
auto config = npc::loadNpcConfig(npcId);
if (!config) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcObject: failed to load npc config %d", npcId);
return false;
}
auto animation = MakePtr<NpcAnimation>();
if (!animation->Init(*config)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpcObject: failed to init npc animation %d", npcId);
return false;
}
SDL_Log("NpcObject: npc %d config loaded name=%s fieldName=%s ani=%s",
npcId, config->name.c_str(), config->fieldName.c_str(),
config->fieldAnimationPath.c_str());
config_ = *config;
npcId_ = npcId;
animation_ = animation;
AddChild(animation_);
EnsureNameLabel();
SetWorldPosition({});
SetDirection(1);
RefreshNameLabel();
SDL_Log("NpcObject: npc %d construction complete", npcId_);
return true;
}
void NpcObject::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1;
if (animation_) {
animation_->SetDirection(direction_);
}
RefreshNameLabel();
}
void NpcObject::SetNpcPosition(const Vec2& pos) {
worldPosition_.x = gameMath::RoundWorldCoordinate(pos.x);
worldPosition_.y = gameMath::RoundWorldCoordinate(pos.y);
SDL_Log("NpcObject: npc %d set ground position to (%d, %d, %d)",
npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z);
SyncActorPositionFromWorld();
}
void NpcObject::SetWorldPosition(const CharacterWorldPosition& pos) {
worldPosition_ = pos;
SDL_Log("NpcObject: npc %d set world position to (%d, %d, %d)",
npcId_, worldPosition_.x, worldPosition_.y, worldPosition_.z);
SyncActorPositionFromWorld();
}
void NpcObject::SetInteractable(bool interactable) {
interactable_ = interactable;
if (!interactable_) {
interacting_ = false;
}
}
void NpcObject::BeginInteract() {
if (CanInteract()) {
interacting_ = true;
}
}
void NpcObject::EndInteract() {
interacting_ = false;
}
bool NpcObject::CanInteract() const {
return interactable_ && config_.has_value();
}
const std::string& NpcObject::GetName() const {
return config_ ? config_->name : EmptyString();
}
const std::string& NpcObject::GetFieldName() const {
return config_ ? config_->fieldName : EmptyString();
}
const std::string& NpcObject::GetDisplayName() const {
const std::string& fieldName = GetFieldName();
if (!fieldName.empty()) {
return fieldName;
}
return GetName();
}
bool NpcObject::IsAnimationFinished() const {
return animation_ ? animation_->IsAnimationFinished() : false;
}
void NpcObject::Update(float deltaTime) {
Actor::Update(deltaTime);
}
void NpcObject::EnsureNameLabel() {
if (nameLabel_) {
return;
}
for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) {
RefPtr<TextSprite> outlineLabel = TextSprite::create();
ConfigureNameTextLabel(outlineLabel, "npcNameOutline",
Color(0.0f, 0.0f, 0.0f, 1.0f), 1);
nameOutlineLabels_[i] = outlineLabel;
AddChild(outlineLabel);
}
nameLabel_ = TextSprite::create();
ConfigureNameTextLabel(nameLabel_, "npcName",
Color(1.0f, 0.96f, 0.78f, 1.0f), 2);
AddChild(nameLabel_);
}
void NpcObject::RefreshNameLabel() {
if (!nameLabel_) {
return;
}
const std::string& displayName = GetDisplayName();
bool hasDisplayName = !displayName.empty();
nameLabel_->SetVisible(hasDisplayName);
for (auto& outlineLabel : nameOutlineLabels_) {
if (outlineLabel) {
outlineLabel->SetVisible(hasDisplayName);
}
}
if (!hasDisplayName) {
return;
}
nameLabel_->SetText(displayName);
for (auto& outlineLabel : nameOutlineLabels_) {
if (outlineLabel) {
outlineLabel->SetText(displayName);
}
}
Rect displayBounds;
float labelBaseY = kNpcNameFallbackTopY;
if (animation_ && animation_->GetDisplayLocalBounds(displayBounds)) {
labelBaseY = displayBounds.top() - kNpcNameMarginY;
}
nameLabel_->SetPosition(0.0f, labelBaseY);
for (size_t i = 0; i < nameOutlineLabels_.size(); ++i) {
if (!nameOutlineLabels_[i]) {
continue;
}
const Vec2& offset = kNpcNameOutlineOffsets[i];
nameOutlineLabels_[i]->SetPosition(offset.x, labelBaseY + offset.y);
}
}
void NpcObject::SyncActorPositionFromWorld() {
SetPosition(worldPosition_.ToScreenPosition());
SetZOrder(worldPosition_.y);
}
} // namespace frostbite2D