feat(角色): 添加角色阴影渲染功能

新增 CharacterShadowActor 类用于处理角色阴影的渲染
在 CharacterObject 中实现阴影的同步和渲染逻辑
移除 GameDebugActor 中不再使用的合成纹理预览代码
添加 EnsureCompositeTextureReady 方法确保纹理准备就绪
This commit is contained in:
2026-04-07 07:08:53 +08:00
parent 808431f92c
commit e570fec599
12 changed files with 259 additions and 218 deletions

View File

@@ -441,6 +441,11 @@ void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
RefreshRuntimeCallbacks();
}
bool CharacterAnimation::EnsureCompositeTextureReady() {
RefreshCompositeTextureIfNeeded();
return HasCompositeTexture();
}
bool CharacterAnimation::HasCompositeTexture() const {
return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
GetCurrentCompositeFrameInfo().valid;

View File

@@ -1,4 +1,5 @@
#include "character/CharacterObject.h"
#include "character/CharacterShadowActor.h"
#include "common/math/GameMath.h"
#include "map/GameMap.h"
#include "world/GameWorld.h"
@@ -43,14 +44,24 @@ Vec3 ToWorldVector(const CharacterWorldPosition& position) {
static_cast<float>(position.z));
}
constexpr int kShadowNormalLayerZBias = 1000000;
} // namespace
CharacterObject::~CharacterObject() {
DetachShadowActor();
shadowActor_ = nullptr;
}
bool CharacterObject::Construction(int jobId) {
ScopedStartupTrace startupTrace("CharacterObject::Construction");
// Reset all runtime state before rebuilding the character from config.
EnableEventReceive();
DetachShadowActor();
RemoveAllChildren();
animationManager_ = nullptr;
shadowActor_ = nullptr;
shadowAttachedMap_ = nullptr;
config_.reset();
currentAction_.clear();
actionLibrary_ = CharacterActionLibrary();
@@ -102,6 +113,9 @@ bool CharacterObject::Construction(int jobId) {
}
AddChild(animationManager_);
shadowActor_ = MakePtr<CharacterShadowActor>();
shadowAttachedMap_ = nullptr;
{
ScopedStartupTrace stageTrace("CharacterObject initial action setup");
if (!RequireAction("idle", "CharacterObject::Construction")) {
@@ -172,6 +186,57 @@ void CharacterObject::SyncActorPositionFromWorld() {
SetZOrder(worldPosition.y);
}
void CharacterObject::DetachShadowActor() {
if (shadowActor_ && shadowActor_->GetParent()) {
shadowActor_->GetParent()->RemoveChild(shadowActor_);
}
shadowAttachedMap_ = nullptr;
}
void CharacterObject::SyncShadowAttachment() {
if (!shadowActor_) {
return;
}
GameMap* map = FindOwningMap();
if (!map) {
DetachShadowActor();
return;
}
if (shadowAttachedMap_ == map && shadowActor_->GetParent()) {
return;
}
if (shadowActor_->GetParent()) {
shadowActor_->GetParent()->RemoveChild(shadowActor_);
}
map->AddObjectToLayer("normal", shadowActor_);
shadowAttachedMap_ = map;
}
void CharacterObject::SyncShadowPresentation() {
if (!shadowActor_) {
return;
}
CharacterShadowFrameSnapshot snapshot;
snapshot.groundScreenPosition = motor_.position.ToGroundPosition();
snapshot.zOrder = motor_.position.y - kShadowNormalLayerZBias;
if (HasCompositeTexture()) {
snapshot.visible = true;
snapshot.texture = GetCompositeTexture();
snapshot.textureSize = GetCompositeTextureSize();
snapshot.groundAnchorInTexture = GetCompositeGroundAnchorInTexture();
snapshot.textureVersion = GetCompositeTextureVersion();
}
shadowActor_->ApplyFrameSnapshot(snapshot);
}
GameMap* CharacterObject::FindOwningMap() const {
Actor* node = GetParent();
while (node) {
@@ -317,6 +382,19 @@ void CharacterObject::ApplyHit(const HitContext& hit) {
stateMachine_.ForceHurt(*this, hurtAction);
}
void CharacterObject::Update(float deltaTime) {
Actor::Update(deltaTime);
SyncShadowAttachment();
SyncShadowPresentation();
}
void CharacterObject::PrepareRenderFrame() {
if (animationManager_) {
animationManager_->EnsureCompositeTextureReady();
}
SyncShadowPresentation();
}
void CharacterObject::OnUpdate(float deltaTime) {
// Fixed update order keeps input, state transitions, and motion deterministic.
lastDeltaTime_ = deltaTime;
@@ -332,6 +410,12 @@ void CharacterObject::OnUpdate(float deltaTime) {
SetFacing(motor_.facing);
}
void CharacterObject::OnAdded(Actor* parent) {
(void)parent;
SyncShadowAttachment();
SyncShadowPresentation();
}
bool CharacterObject::OnEvent(const Event& event) {
if (!IsEventReceiveEnabled() || !inputEnabled_) {
return false;

View File

@@ -0,0 +1,61 @@
#include "character/CharacterShadowActor.h"
#include <algorithm>
namespace frostbite2D {
namespace {
constexpr Color kShadowColor(0.0f, 0.0f, 0.0f, 0.42f);
const Vec2 kShadowScale(0.9f, 0.30f);
constexpr float kShadowSkewX = 50.0f;
} // namespace
CharacterShadowActor::CharacterShadowActor() {
SetName("characterShadow");
SetColor(kShadowColor);
SetScale(kShadowScale);
// Anchor stays on the ground point while the upper silhouette falls left.
SetSkew(kShadowSkewX, 0.0f);
SetBlendMode(BlendMode::Normal);
SetVisible(false);
}
void CharacterShadowActor::ApplyFrameSnapshot(
const CharacterShadowFrameSnapshot& snapshot) {
bool hasTexture = snapshot.visible && snapshot.texture && snapshot.textureSize.x > 0.0f &&
snapshot.textureSize.y > 0.0f;
if (!hasTexture) {
SetVisible(false);
return;
}
ApplyTextureState(snapshot);
SetPosition(snapshot.groundScreenPosition);
SetZOrder(snapshot.zOrder);
SetVisible(true);
}
void CharacterShadowActor::ApplyTextureState(
const CharacterShadowFrameSnapshot& snapshot) {
if (appliedTexture_ == snapshot.texture &&
appliedTextureSize_ == snapshot.textureSize &&
appliedGroundAnchorInTexture_ == snapshot.groundAnchorInTexture &&
appliedTextureVersion_ == snapshot.textureVersion) {
return;
}
SetTexture(snapshot.texture);
SetSize(snapshot.textureSize);
float safeWidth = std::max(snapshot.textureSize.x, 1.0f);
float safeHeight = std::max(snapshot.textureSize.y, 1.0f);
SetAnchor(std::clamp(snapshot.groundAnchorInTexture.x / safeWidth, 0.0f, 1.0f),
std::clamp(snapshot.groundAnchorInTexture.y / safeHeight, 0.0f, 1.0f));
appliedTexture_ = snapshot.texture;
appliedTextureSize_ = snapshot.textureSize;
appliedGroundAnchorInTexture_ = snapshot.groundAnchorInTexture;
appliedTextureVersion_ = snapshot.textureVersion;
}
} // namespace frostbite2D