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 "camera/GameCameraController.h"
|
||||||
#include "character/CharacterObject.h"
|
#include "character/CharacterObject.h"
|
||||||
#include "map/GameMap.h"
|
#include "map/GameMap.h"
|
||||||
|
#include "npc/NpcObject.h"
|
||||||
#include "scene/GameDebugUIScene.h"
|
#include "scene/GameDebugUIScene.h"
|
||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
GameCameraController cameraController_;
|
GameCameraController cameraController_;
|
||||||
RefPtr<CharacterObject> character_;
|
RefPtr<CharacterObject> character_;
|
||||||
|
RefPtr<NpcObject> npc_;
|
||||||
RefPtr<GameDebugUIScene> debugScene_;
|
RefPtr<GameDebugUIScene> debugScene_;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
RefPtr<GameMap> map_;
|
RefPtr<GameMap> map_;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
#include <frostbite2D/utils/startup_trace.h>
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include "world/GameWorld.h"
|
#include "scene/GameMapTestScene.h"
|
||||||
|
|
||||||
using namespace frostbite2D;
|
using namespace frostbite2D;
|
||||||
|
|
||||||
@@ -154,9 +154,10 @@ int main(int argc, char **argv) {
|
|||||||
StartupTrace::mark("before SceneManager::ReplaceScene");
|
StartupTrace::mark("before SceneManager::ReplaceScene");
|
||||||
|
|
||||||
{
|
{
|
||||||
ScopedStartupTrace stageTrace("SceneManager::ReplaceScene(GameWorld)");
|
ScopedStartupTrace stageTrace(
|
||||||
auto gameWorld = MakePtr<GameWorld>();
|
"SceneManager::ReplaceScene(GameMapTestScene)");
|
||||||
SceneManager::get().ReplaceScene(gameWorld);
|
auto gameMapTestScene = MakePtr<GameMapTestScene>();
|
||||||
|
SceneManager::get().ReplaceScene(gameMapTestScene);
|
||||||
}
|
}
|
||||||
|
|
||||||
StartupTrace::mark("after SceneManager::ReplaceScene");
|
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 {
|
namespace {
|
||||||
|
|
||||||
constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map";
|
constexpr char kTestMapPath[] = "map/elvengard/d_elvengard.map";
|
||||||
|
constexpr int kTestNpcId = 2;
|
||||||
|
constexpr float kTestNpcOffsetX = 180.0f;
|
||||||
|
constexpr float kTestNpcOffsetY = -24.0f;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -43,9 +46,9 @@ void GameMapTestScene::onEnter() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
ScopedStartupTrace stageTrace("GameMapTestScene character construction");
|
ScopedStartupTrace stageTrace("GameMapTestScene character construction");
|
||||||
character_ = MakePtr<CharacterObject>();
|
character_ = MakePtr<CharacterObject>();
|
||||||
if (!character_->Construction(0)) {
|
if (!character_->Construction(0)) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"GameMapTestScene: failed to construct default character");
|
"GameMapTestScene: failed to construct default character");
|
||||||
character_.Reset();
|
character_.Reset();
|
||||||
} else {
|
} else {
|
||||||
@@ -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_.SetMap(map_.Get());
|
||||||
cameraController_.SetTarget(character_.Get());
|
cameraController_.SetTarget(character_.Get());
|
||||||
cameraController_.SetDebugEnabled(false);
|
cameraController_.SetDebugEnabled(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user