实现NPC交互时的绿色高亮效果,通过InteractionHighlightSprite类实现 重构NpcAnimation支持复合纹理渲染,优化高亮效果的性能 添加ShaderManager获取所有加载Shader的方法,优化渲染器uniform更新逻辑
279 lines
7.4 KiB
C++
279 lines
7.4 KiB
C++
#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;
|
|
interactionHighlight_ = nullptr;
|
|
nameOutlineLabels_.fill(nullptr);
|
|
nameLabel_ = nullptr;
|
|
interactable_ = true;
|
|
interacting_ = false;
|
|
interactionHighlighted_ = false;
|
|
syncedHighlightCompositeVersion_ = 0;
|
|
|
|
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;
|
|
EnsureInteractionHighlight();
|
|
AddChild(animation_);
|
|
EnsureNameLabel();
|
|
|
|
SetWorldPosition({});
|
|
SetDirection(1);
|
|
SetInteractionHighlighted(false);
|
|
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();
|
|
SyncInteractionHighlight();
|
|
}
|
|
|
|
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::SetInteractionHighlighted(bool highlighted) {
|
|
if (interactionHighlighted_ == highlighted) {
|
|
SyncInteractionHighlight();
|
|
return;
|
|
}
|
|
|
|
interactionHighlighted_ = highlighted;
|
|
if (!interactionHighlighted_) {
|
|
syncedHighlightCompositeVersion_ = 0;
|
|
}
|
|
SyncInteractionHighlight();
|
|
}
|
|
|
|
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);
|
|
SyncInteractionHighlight();
|
|
}
|
|
|
|
void NpcObject::EnsureInteractionHighlight() {
|
|
if (interactionHighlight_) {
|
|
return;
|
|
}
|
|
|
|
interactionHighlight_ = MakePtr<InteractionHighlightSprite>();
|
|
interactionHighlight_->SetZOrder(-1);
|
|
interactionHighlight_->SetPosition(0.0f, 0.0f);
|
|
interactionHighlight_->SetVisible(false);
|
|
AddChild(interactionHighlight_);
|
|
}
|
|
|
|
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::SyncInteractionHighlight() {
|
|
if (!interactionHighlight_) {
|
|
return;
|
|
}
|
|
|
|
if (!interactionHighlighted_ || !animation_ ||
|
|
!animation_->EnsureCompositeTextureReady() ||
|
|
!animation_->HasCompositeTexture()) {
|
|
interactionHighlight_->SetVisible(false);
|
|
return;
|
|
}
|
|
|
|
uint64 compositeVersion = animation_->GetCompositeVersion();
|
|
if (syncedHighlightCompositeVersion_ != compositeVersion) {
|
|
Ptr<Texture> compositeTexture = animation_->GetCompositeTexture();
|
|
Vec2 compositeSize = animation_->GetCompositeTextureSize();
|
|
if (!compositeTexture || compositeSize.x <= 0.0f || compositeSize.y <= 0.0f) {
|
|
interactionHighlight_->SetVisible(false);
|
|
return;
|
|
}
|
|
|
|
interactionHighlight_->SetTexture(compositeTexture);
|
|
interactionHighlight_->SetSize(compositeSize);
|
|
interactionHighlight_->SetGroundAnchorInTexture(
|
|
animation_->GetCompositeGroundAnchorInTexture());
|
|
interactionHighlight_->SetPosition(0.0f, 0.0f);
|
|
syncedHighlightCompositeVersion_ = compositeVersion;
|
|
}
|
|
|
|
interactionHighlight_->SetVisible(true);
|
|
}
|
|
|
|
void NpcObject::SyncActorPositionFromWorld() {
|
|
SetPosition(worldPosition_.ToScreenPosition());
|
|
SetZOrder(worldPosition_.y);
|
|
}
|
|
|
|
} // namespace frostbite2D
|