feat(npc): 添加NPC数据加载、动画和对象功能
实现NPC系统的核心功能,包括: 1. 新增NpcDataLoader用于加载NPC索引和配置数据 2. 添加NpcAnimation处理NPC动画显示 3. 创建NpcObject实现NPC交互和显示逻辑 4. 在GameMapTestScene中集成测试NPC功能
This commit is contained in:
214
Game/src/npc/NpcObject.cpp
Normal file
214
Game/src/npc/NpcObject.cpp
Normal 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
|
||||
Reference in New Issue
Block a user