improve dialogue advance and add flexible cutscene camera system
This commit is contained in:
1 parent
422ff1fbe3
commit
ceebb13719
10 files changed
+502
-74
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include "dialogue/DialogueRuntime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
@@ -30,6 +31,8 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
||||
revealCharacters = 0.0f;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
cutsceneElapsedMs = 0;
|
||||
cutsceneTotalDurationMs = 0;
|
||||
presentation = {};
|
||||
presentation.dialogueId = dialogue->id;
|
||||
|
||||
@@ -46,6 +49,8 @@ void DialogueRuntime::stop() {
|
||||
revealCharacters = 0.0f;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
cutsceneElapsedMs = 0;
|
||||
cutsceneTotalDurationMs = 0;
|
||||
mode = Mode::Inactive;
|
||||
presentation = {};
|
||||
}
|
||||
@@ -63,20 +68,59 @@ void DialogueRuntime::update(int deltaMs) {
|
||||
}
|
||||
|
||||
if (mode == Mode::PlayingCutscene && activeCutscene) {
|
||||
if (currentCutsceneLine < 0 || currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
advanceCutsceneLine();
|
||||
cutsceneElapsedMs += deltaMs;
|
||||
|
||||
if (!activeCutscene->lines.empty() &&
|
||||
currentCutsceneLine >= 0 &&
|
||||
currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
if (!line.waitForConfirm) {
|
||||
cutsceneTimerMs += deltaMs;
|
||||
const int durationMs =
|
||||
(line.durationMs > 0)
|
||||
? line.durationMs
|
||||
: computeFallbackCutsceneDurationMs(line.text);
|
||||
|
||||
if (cutsceneTimerMs >= durationMs) {
|
||||
advanceCutsceneLine();
|
||||
|
||||
// ВАЖНО: после advance катсцена могла завершиться
|
||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
if (line.waitForConfirm) {
|
||||
refreshCutscenePresentation();
|
||||
|
||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
||||
return;
|
||||
}
|
||||
|
||||
cutsceneTimerMs += deltaMs;
|
||||
const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text);
|
||||
if (cutsceneTimerMs >= durationMs) {
|
||||
advanceCutsceneLine();
|
||||
const bool subtitlesFinished =
|
||||
activeCutscene->lines.empty() ||
|
||||
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size());
|
||||
|
||||
const bool durationFinished =
|
||||
cutsceneTotalDurationMs > 0 &&
|
||||
cutsceneElapsedMs >= cutsceneTotalDurationMs;
|
||||
|
||||
if (activeCutscene->lines.empty()) {
|
||||
if (durationFinished) {
|
||||
finishCutscene();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) {
|
||||
finishCutscene();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +169,13 @@ void DialogueRuntime::confirmAdvance() {
|
||||
}
|
||||
|
||||
if (mode == Mode::PlayingCutscene) {
|
||||
advanceCutsceneLine();
|
||||
if (!activeCutscene || activeCutscene->lines.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
|
||||
advanceCutsceneLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +311,8 @@ void DialogueRuntime::presentLine(const Node& node) {
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = node.text.empty();
|
||||
presentation.showCutsceneSubtitle = false;
|
||||
presentation.cutsceneCamera = {};
|
||||
|
||||
if (presentation.revealCompleted) {
|
||||
presentation.visibleText = node.text;
|
||||
@@ -303,6 +355,8 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
||||
presentation.backgroundPath.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = true;
|
||||
presentation.showCutsceneSubtitle = false;
|
||||
presentation.cutsceneCamera = {};
|
||||
}
|
||||
|
||||
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
|
||||
@@ -326,9 +380,37 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
|
||||
activeCutscene = cutscene;
|
||||
pendingNodeAfterCutscene = nextNodeAfterCutscene;
|
||||
mode = Mode::PlayingCutscene;
|
||||
cutsceneElapsedMs = 0;
|
||||
cutsceneTimerMs = 0;
|
||||
currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0;
|
||||
cutsceneTotalDurationMs = std::max(activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene));
|
||||
if (cutsceneTotalDurationMs <= 0 && activeCutscene->lines.empty()) {
|
||||
cutsceneTotalDurationMs = 3000;
|
||||
}
|
||||
refreshCutscenePresentation();
|
||||
|
||||
std::cout << "[CUTSCENE] start id=" << cutsceneId
|
||||
<< " lines=" << activeCutscene->lines.size()
|
||||
<< " totalDuration=" << cutsceneTotalDurationMs
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
void DialogueRuntime::finishCutscene() {
|
||||
|
||||
std::cout << "[CUTSCENE] finish nextNode=" << pendingNodeAfterCutscene << std::endl;
|
||||
activeCutscene = nullptr;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
advanceCutsceneLine();
|
||||
cutsceneElapsedMs = 0;
|
||||
cutsceneTotalDurationMs = 0;
|
||||
if (!pendingNodeAfterCutscene.empty()) {
|
||||
const std::string nextNode = pendingNodeAfterCutscene;
|
||||
pendingNodeAfterCutscene.clear();
|
||||
enterNode(nextNode);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueRuntime::advanceCutsceneLine() {
|
||||
@@ -337,18 +419,19 @@ void DialogueRuntime::advanceCutsceneLine() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeCutscene->lines.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl;
|
||||
++currentCutsceneLine;
|
||||
std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl;
|
||||
cutsceneTimerMs = 0;
|
||||
|
||||
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
activeCutscene = nullptr;
|
||||
if (!pendingNodeAfterCutscene.empty()) {
|
||||
const std::string nextNode = pendingNodeAfterCutscene;
|
||||
pendingNodeAfterCutscene.clear();
|
||||
enterNode(nextNode);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
refreshCutscenePresentation();
|
||||
if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) {
|
||||
finishCutscene();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -356,22 +439,101 @@ void DialogueRuntime::advanceCutsceneLine() {
|
||||
refreshCutscenePresentation();
|
||||
}
|
||||
|
||||
CutsceneCameraPose DialogueRuntime::evaluateCutsceneCameraPose() const {
|
||||
CutsceneCameraPose defaultPose{};
|
||||
if (!activeCutscene || activeCutscene->cameraTrack.empty()) {
|
||||
return defaultPose;
|
||||
}
|
||||
|
||||
int elapsed = cutsceneElapsedMs;
|
||||
for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) {
|
||||
const int durationMs = std::max(segment.durationMs, 1);
|
||||
if (elapsed <= durationMs) {
|
||||
const float rawT = static_cast<float>(elapsed) / static_cast<float>(durationMs);
|
||||
const float t = applyEasing(segment.easing, std::clamp(rawT, 0.0f, 1.0f));
|
||||
CutsceneCameraPose pose;
|
||||
pose.focusX = segment.from.focusX + (segment.to.focusX - segment.from.focusX) * t;
|
||||
pose.focusY = segment.from.focusY + (segment.to.focusY - segment.from.focusY) * t;
|
||||
pose.zoom = segment.from.zoom + (segment.to.zoom - segment.from.zoom) * t;
|
||||
pose.rotationDeg = segment.from.rotationDeg + (segment.to.rotationDeg - segment.from.rotationDeg) * t;
|
||||
return pose;
|
||||
}
|
||||
elapsed -= durationMs;
|
||||
}
|
||||
|
||||
return activeCutscene->cameraTrack.back().to;
|
||||
}
|
||||
|
||||
void DialogueRuntime::refreshCutscenePresentation() {
|
||||
if (!activeCutscene || currentCutsceneLine < 0 ||
|
||||
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
if (!activeCutscene) {
|
||||
return;
|
||||
}
|
||||
|
||||
presentation.mode = PresentationMode::Cutscene;
|
||||
presentation.backgroundPath = activeCutscene->background;
|
||||
presentation.cutsceneCamera = evaluateCutsceneCameraPose();
|
||||
|
||||
std::cout << "[CUTSCENE] pose focus=("
|
||||
<< presentation.cutsceneCamera.focusX << ", "
|
||||
<< presentation.cutsceneCamera.focusY << ") zoom="
|
||||
<< presentation.cutsceneCamera.zoom
|
||||
<< " rot=" << presentation.cutsceneCamera.rotationDeg
|
||||
<< " line=" << currentCutsceneLine
|
||||
<< std::endl;
|
||||
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = true;
|
||||
|
||||
const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
|
||||
presentation.showCutsceneSubtitle = hasSubtitle;
|
||||
|
||||
if (!hasSubtitle) {
|
||||
presentation.speaker.clear();
|
||||
presentation.fullText.clear();
|
||||
presentation.visibleText.clear();
|
||||
presentation.portraitPath.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
presentation.mode = PresentationMode::Cutscene;
|
||||
presentation.speaker = line.speaker;
|
||||
presentation.fullText = line.text;
|
||||
presentation.visibleText = line.text;
|
||||
presentation.portraitPath = line.portrait;
|
||||
presentation.backgroundPath = activeCutscene->background;
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = true;
|
||||
|
||||
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
||||
<< " current=" << currentCutsceneLine
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
float DialogueRuntime::applyEasing(EasingType easing, float t) {
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
|
||||
switch (easing) {
|
||||
case EasingType::EaseInSine:
|
||||
return 1.0f - std::cos((t * PI) * 0.5f);
|
||||
case EasingType::EaseOutSine:
|
||||
return std::sin((t * PI) * 0.5f);
|
||||
case EasingType::EaseInOutSine:
|
||||
return -(std::cos(PI * t) - 1.0f) * 0.5f;
|
||||
case EasingType::EaseInQuad:
|
||||
return t * t;
|
||||
case EasingType::EaseOutQuad:
|
||||
return 1.0f - (1.0f - t) * (1.0f - t);
|
||||
case EasingType::EaseInOutQuad:
|
||||
return (t < 0.5f) ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) * 0.5f;
|
||||
case EasingType::EaseInCubic:
|
||||
return t * t * t;
|
||||
case EasingType::EaseOutCubic:
|
||||
return 1.0f - std::pow(1.0f - t, 3.0f);
|
||||
case EasingType::EaseInOutCubic:
|
||||
return (t < 0.5f) ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) * 0.5f;
|
||||
case EasingType::Linear:
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
||||
@@ -382,6 +544,14 @@ int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text)
|
||||
return std::max(minDuration, calculated + linger);
|
||||
}
|
||||
|
||||
int DialogueRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) {
|
||||
int total = 0;
|
||||
for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) {
|
||||
total += std::max(segment.durationMs, 0);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
DialogueRuntime::json DialogueRuntime::buildSaveState() const {
|
||||
json result;
|
||||
result["active"] = isActive();
|
||||
|
||||
Reference in new issue
Block a user