feat(npc): 添加NPC数据加载、动画和对象功能
实现NPC系统的核心功能,包括: 1. 新增NpcDataLoader用于加载NPC索引和配置数据 2. 添加NpcAnimation处理NPC动画显示 3. 创建NpcObject实现NPC交互和显示逻辑 4. 在GameMapTestScene中集成测试NPC功能
This commit is contained in:
25
Game/include/npc/NpcAnimation.h
Normal file
25
Game/include/npc/NpcAnimation.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "npc/NpcDataLoader.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/animation/animation.h>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class NpcAnimation : public Actor {
|
||||
public:
|
||||
bool Init(const npc::NpcConfig& config);
|
||||
void SetDirection(int direction);
|
||||
bool IsReady() const { return displayAnimation_ != nullptr; }
|
||||
bool IsAnimationFinished() const;
|
||||
const std::string& GetAnimationPath() const { return animationPath_; }
|
||||
bool GetDisplayLocalBounds(Rect& outBounds) const;
|
||||
|
||||
private:
|
||||
RefPtr<Animation> displayAnimation_ = nullptr;
|
||||
std::string animationPath_;
|
||||
int direction_ = 1;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
20
Game/include/npc/NpcDataLoader.h
Normal file
20
Game/include/npc/NpcDataLoader.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D::npc {
|
||||
|
||||
struct NpcConfig {
|
||||
int id = -1;
|
||||
std::string path;
|
||||
std::string name;
|
||||
std::string fieldName;
|
||||
std::string fieldAnimationPath;
|
||||
};
|
||||
|
||||
bool loadNpcIndex(std::map<int, std::string>& outIndex);
|
||||
std::optional<NpcConfig> loadNpcConfig(int npcId);
|
||||
|
||||
} // namespace frostbite2D::npc
|
||||
59
Game/include/npc/NpcObject.h
Normal file
59
Game/include/npc/NpcObject.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "character/CharacterActionTypes.h"
|
||||
#include "npc/NpcAnimation.h"
|
||||
#include <frostbite2D/2d/text_sprite.h>
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class NpcObject : public Actor {
|
||||
public:
|
||||
NpcObject() = default;
|
||||
~NpcObject() override = default;
|
||||
|
||||
bool Construction(int npcId);
|
||||
|
||||
void SetDirection(int direction);
|
||||
void SetNpcPosition(const Vec2& pos);
|
||||
void SetWorldPosition(const CharacterWorldPosition& pos);
|
||||
void SetInteractable(bool interactable);
|
||||
void BeginInteract();
|
||||
void EndInteract();
|
||||
|
||||
bool CanInteract() const;
|
||||
bool IsInteracting() const { return interacting_; }
|
||||
int GetNpcId() const { return npcId_; }
|
||||
int GetDirection() const { return direction_; }
|
||||
const std::string& GetName() const;
|
||||
const std::string& GetFieldName() const;
|
||||
const std::string& GetDisplayName() const;
|
||||
const CharacterWorldPosition& GetWorldPosition() const { return worldPosition_; }
|
||||
bool IsAnimationFinished() const;
|
||||
|
||||
const npc::NpcConfig* GetConfig() const {
|
||||
return config_ ? &config_.value() : nullptr;
|
||||
}
|
||||
|
||||
void Update(float deltaTime) override;
|
||||
|
||||
private:
|
||||
void EnsureNameLabel();
|
||||
void RefreshNameLabel();
|
||||
void SyncActorPositionFromWorld();
|
||||
|
||||
int npcId_ = -1;
|
||||
int direction_ = 1;
|
||||
CharacterWorldPosition worldPosition_;
|
||||
std::optional<npc::NpcConfig> config_;
|
||||
RefPtr<NpcAnimation> animation_ = nullptr;
|
||||
std::array<RefPtr<TextSprite>, 8> nameOutlineLabels_;
|
||||
RefPtr<TextSprite> nameLabel_ = nullptr;
|
||||
bool interactable_ = true;
|
||||
bool interacting_ = false;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "camera/GameCameraController.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include "map/GameMap.h"
|
||||
#include "npc/NpcObject.h"
|
||||
#include "scene/GameDebugUIScene.h"
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
|
||||
@@ -20,6 +21,7 @@ public:
|
||||
private:
|
||||
GameCameraController cameraController_;
|
||||
RefPtr<CharacterObject> character_;
|
||||
RefPtr<NpcObject> npc_;
|
||||
RefPtr<GameDebugUIScene> debugScene_;
|
||||
bool initialized_ = false;
|
||||
RefPtr<GameMap> map_;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
#include "world/GameWorld.h"
|
||||
#include "scene/GameMapTestScene.h"
|
||||
|
||||
using namespace frostbite2D;
|
||||
|
||||
@@ -154,9 +154,10 @@ int main(int argc, char **argv) {
|
||||
StartupTrace::mark("before SceneManager::ReplaceScene");
|
||||
|
||||
{
|
||||
ScopedStartupTrace stageTrace("SceneManager::ReplaceScene(GameWorld)");
|
||||
auto gameWorld = MakePtr<GameWorld>();
|
||||
SceneManager::get().ReplaceScene(gameWorld);
|
||||
ScopedStartupTrace stageTrace(
|
||||
"SceneManager::ReplaceScene(GameMapTestScene)");
|
||||
auto gameMapTestScene = MakePtr<GameMapTestScene>();
|
||||
SceneManager::get().ReplaceScene(gameMapTestScene);
|
||||
}
|
||||
|
||||
StartupTrace::mark("after SceneManager::ReplaceScene");
|
||||
|
||||
58
Game/src/npc/NpcAnimation.cpp
Normal file
58
Game/src/npc/NpcAnimation.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "npc/NpcAnimation.h"
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
bool NpcAnimation::Init(const npc::NpcConfig& config) {
|
||||
RemoveAllChildren();
|
||||
displayAnimation_ = nullptr;
|
||||
animationPath_.clear();
|
||||
direction_ = 1;
|
||||
|
||||
if (config.fieldAnimationPath.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcAnimation: npc %d missing field animation path", config.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto animation = MakePtr<Animation>(config.fieldAnimationPath);
|
||||
if (!animation || !animation->IsUsable()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcAnimation: failed to load animation %s for npc %d",
|
||||
config.fieldAnimationPath.c_str(), config.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
animationPath_ = config.fieldAnimationPath;
|
||||
SDL_Log("NpcAnimation: npc %d loaded display animation %s",
|
||||
config.id, animationPath_.c_str());
|
||||
|
||||
animation->SetVisible(true);
|
||||
AddChild(animation);
|
||||
displayAnimation_ = animation;
|
||||
SetDirection(1);
|
||||
displayAnimation_->Reset();
|
||||
SetDirection(direction_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void NpcAnimation::SetDirection(int direction) {
|
||||
direction_ = direction >= 0 ? 1 : -1;
|
||||
if (displayAnimation_) {
|
||||
displayAnimation_->SetDirection(direction_);
|
||||
}
|
||||
}
|
||||
|
||||
bool NpcAnimation::IsAnimationFinished() const {
|
||||
return displayAnimation_ ? !displayAnimation_->IsUsable() : false;
|
||||
}
|
||||
|
||||
bool NpcAnimation::GetDisplayLocalBounds(Rect& outBounds) const {
|
||||
if (!displayAnimation_) {
|
||||
outBounds = Rect();
|
||||
return false;
|
||||
}
|
||||
return displayAnimation_->GetStaticLocalBounds(outBounds);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
217
Game/src/npc/NpcDataLoader.cpp
Normal file
217
Game/src/npc/NpcDataLoader.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "npc/NpcDataLoader.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <frostbite2D/resource/script_parser.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D::npc {
|
||||
namespace {
|
||||
|
||||
constexpr char kNpcListPath[] = "npc/npc.lst";
|
||||
constexpr char kTagName[] = "[name]";
|
||||
constexpr char kTagFieldName[] = "[field name]";
|
||||
constexpr char kTagFieldAnimation[] = "[field animation]";
|
||||
|
||||
std::string toLowerCase(const std::string& str) {
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& value) {
|
||||
size_t start = 0;
|
||||
size_t end = value.size();
|
||||
while (start < end && std::isspace(static_cast<unsigned char>(value[start]))) {
|
||||
++start;
|
||||
}
|
||||
while (end > start &&
|
||||
std::isspace(static_cast<unsigned char>(value[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
return value.substr(start, end - start);
|
||||
}
|
||||
|
||||
int toInt(const std::string& value, int fallback = 0) {
|
||||
try {
|
||||
return std::stoi(value);
|
||||
} catch (...) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
bool isToken(const std::string& token, const char* expected) {
|
||||
return trim(token) == expected;
|
||||
}
|
||||
|
||||
std::string normalizeNpcPath(const std::string& path) {
|
||||
auto& pvf = PvfArchive::get();
|
||||
std::string normalized = pvf.normalizePath(path);
|
||||
if (normalized.rfind("npc/", 0) == 0) {
|
||||
return normalized;
|
||||
}
|
||||
return pvf.normalizePath("npc/" + normalized);
|
||||
}
|
||||
|
||||
void logTokenPreview(const char* label, const std::vector<std::string>& tokens) {
|
||||
std::string preview;
|
||||
size_t previewCount = std::min<size_t>(tokens.size(), 8);
|
||||
for (size_t i = 0; i < previewCount; ++i) {
|
||||
if (!preview.empty()) {
|
||||
preview += " | ";
|
||||
}
|
||||
preview += tokens[i];
|
||||
}
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "%s: %s", label, preview.c_str());
|
||||
}
|
||||
|
||||
class ScriptTokenStream {
|
||||
public:
|
||||
explicit ScriptTokenStream(const std::string& path)
|
||||
: path_(toLowerCase(path)) {
|
||||
auto& pvf = PvfArchive::get();
|
||||
auto rawData = pvf.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 get() {
|
||||
if (isEnd()) {
|
||||
return {};
|
||||
}
|
||||
return tokens_[index_++];
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
std::vector<std::string> tokens_;
|
||||
size_t index_ = 0;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
std::map<int, std::string>& npcIndexCache() {
|
||||
static std::map<int, std::string> cache;
|
||||
static bool loaded = false;
|
||||
if (!loaded) {
|
||||
ScriptTokenStream stream(kNpcListPath);
|
||||
if (stream.isValid()) {
|
||||
while (!stream.isEnd()) {
|
||||
std::string indexToken = stream.get();
|
||||
if (indexToken.empty()) {
|
||||
break;
|
||||
}
|
||||
std::string pathToken = stream.get();
|
||||
if (pathToken.empty()) {
|
||||
break;
|
||||
}
|
||||
cache[toInt(indexToken, -1)] = normalizeNpcPath(pathToken);
|
||||
}
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcDataLoader: npc index %s not found", kNpcListPath);
|
||||
}
|
||||
loaded = true;
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::map<int, NpcConfig>& npcConfigCache() {
|
||||
static std::map<int, NpcConfig> cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool loadNpcIndex(std::map<int, std::string>& outIndex) {
|
||||
outIndex = npcIndexCache();
|
||||
if (!outIndex.empty()) {
|
||||
SDL_Log("NpcDataLoader: loaded npc index entries=%d",
|
||||
static_cast<int>(outIndex.size()));
|
||||
}
|
||||
return !outIndex.empty();
|
||||
}
|
||||
|
||||
std::optional<NpcConfig> loadNpcConfig(int npcId) {
|
||||
auto cacheIt = npcConfigCache().find(npcId);
|
||||
if (cacheIt != npcConfigCache().end()) {
|
||||
return cacheIt->second;
|
||||
}
|
||||
|
||||
auto indexIt = npcIndexCache().find(npcId);
|
||||
if (indexIt == npcIndexCache().end()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcDataLoader: npc %d not found in index", npcId);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ScriptTokenStream stream(indexIt->second);
|
||||
if (!stream.isValid()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcDataLoader: unable to load npc config %s",
|
||||
indexIt->second.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NpcConfig config;
|
||||
config.id = npcId;
|
||||
config.path = indexIt->second;
|
||||
|
||||
std::vector<std::string> tokenPreview;
|
||||
while (!stream.isEnd()) {
|
||||
std::string segment = stream.get();
|
||||
if (tokenPreview.size() < 8) {
|
||||
tokenPreview.push_back(segment);
|
||||
}
|
||||
|
||||
if (isToken(segment, kTagName)) {
|
||||
config.name = stream.get();
|
||||
} else if (isToken(segment, kTagFieldName)) {
|
||||
config.fieldName = stream.get();
|
||||
} else if (isToken(segment, kTagFieldAnimation)) {
|
||||
config.fieldAnimationPath = normalizeNpcPath(stream.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (config.fieldName.empty()) {
|
||||
config.fieldName = config.name;
|
||||
}
|
||||
if (config.name.empty()) {
|
||||
config.name = config.fieldName;
|
||||
}
|
||||
|
||||
if (config.fieldAnimationPath.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpcDataLoader: npc %d config incomplete (name=%s, fieldName=%s, script=%s)",
|
||||
npcId, config.name.c_str(), config.fieldName.c_str(),
|
||||
indexIt->second.c_str());
|
||||
logTokenPreview("NpcDataLoader token preview", tokenPreview);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SDL_Log("NpcDataLoader: npc %d path=%s fieldAnimation=%s",
|
||||
npcId, config.path.c_str(), config.fieldAnimationPath.c_str());
|
||||
|
||||
npcConfigCache()[npcId] = config;
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace frostbite2D::npc
|
||||
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
|
||||
@@ -8,6 +8,9 @@ namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map";
|
||||
constexpr int kTestNpcId = 2;
|
||||
constexpr float kTestNpcOffsetX = 180.0f;
|
||||
constexpr float kTestNpcOffsetY = -24.0f;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -58,6 +61,28 @@ void GameMapTestScene::onEnter() {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ScopedStartupTrace stageTrace("GameMapTestScene npc construction");
|
||||
npc_ = MakePtr<NpcObject>();
|
||||
if (!npc_->Construction(kTestNpcId)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameMapTestScene: failed to construct npc %d", kTestNpcId);
|
||||
npc_.Reset();
|
||||
} else {
|
||||
Vec2 npcPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.0f);
|
||||
if (character_) {
|
||||
npcPos = character_->GetWorldPosition().ToGroundPosition() +
|
||||
Vec2(kTestNpcOffsetX, kTestNpcOffsetY);
|
||||
}
|
||||
npc_->SetNpcPosition(npcPos);
|
||||
npc_->SetDirection(-1);
|
||||
SDL_Log("GameMapTestScene: npc %d final ground position (%d, %d, %d)",
|
||||
kTestNpcId, npc_->GetWorldPosition().x, npc_->GetWorldPosition().y,
|
||||
npc_->GetWorldPosition().z);
|
||||
map_->AddObject(npc_);
|
||||
}
|
||||
}
|
||||
|
||||
cameraController_.SetMap(map_.Get());
|
||||
cameraController_.SetTarget(character_.Get());
|
||||
cameraController_.SetDebugEnabled(false);
|
||||
|
||||
Reference in New Issue
Block a user