From 24c87da4fda0a2b3647fee1997ac2cefd73e364a Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Fri, 4 Nov 2022 00:00:55 +0530 Subject: [PATCH] really basic replay support --- src/CameraController.hx | 7 ++ src/DtsObject.hx | 2 + src/Marble.hx | 26 +++++- src/MarbleWorld.hx | 101 ++++++++++++++++----- src/Replay.hx | 191 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 22 deletions(-) create mode 100644 src/Replay.hx diff --git a/src/CameraController.hx b/src/CameraController.hx index 2a3db8d6..308001b2 100644 --- a/src/CameraController.hx +++ b/src/CameraController.hx @@ -213,6 +213,13 @@ class CameraController extends Object { CameraYaw = this.level.finishYaw - (this.level.timeState.currentAttemptTime - this.level.finishTime.currentAttemptTime) / -1.2; } + if (!this.level.isWatching) { + this.level.replay.recordCameraState(CameraPitch, CameraYaw); + } else { + CameraPitch = this.level.replay.currentPlaybackFrame.cameraPitch; + CameraYaw = this.level.replay.currentPlaybackFrame.cameraYaw; + } + var marblePosition = level.marble.getAbsPos().getPosition(); var up = new Vector(0, 0, 1); up.transform(orientationQuat.toMatrix()); diff --git a/src/DtsObject.hx b/src/DtsObject.hx index 2614dcdc..b1a88a9b 100644 --- a/src/DtsObject.hx +++ b/src/DtsObject.hx @@ -108,6 +108,8 @@ class DtsObject extends GameObject { var ambientRotate = false; var ambientSpinFactor = -1 / 3 * Math.PI * 2; + public var idInLevel:Int = -1; + public function new() { super(); } diff --git a/src/Marble.hx b/src/Marble.hx index d4506c9d..c9efc2bc 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1335,7 +1335,7 @@ class Marble extends GameObject { public function update(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array) { var move = new Move(); move.d = new Vector(); - if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused) { + if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching) { if (Key.isDown(Settings.controlsSettings.forward)) { move.d.x -= 1; } @@ -1360,9 +1360,33 @@ class Marble extends GameObject { } } + if (this.level.isWatching) { + if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(Jumped)) + move.jump = true; + if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(UsedPowerup)) + move.powerup = true; + move.d = new Vector(this.level.replay.currentPlaybackFrame.marbleX, this.level.replay.currentPlaybackFrame.marbleY, 0); + } else { + this.level.replay.recordMarbleStateFlags(move.jump, move.powerup, false); + this.level.replay.recordMarbleInput(move.d.x, move.d.y); + } + playedSounds = []; advancePhysics(timeState, move, collisionWorld, pathedInteriors); + if (!this.level.isWatching) + this.level.replay.recordMarbleState(this.getAbsPos().getPosition(), this.velocity, this.getRotationQuat(), this.omega); + else { + var expectedPos = this.level.replay.currentPlaybackFrame.marblePosition.clone(); + var expectedVel = this.level.replay.currentPlaybackFrame.marbleVelocity.clone(); + var expectedOmega = this.level.replay.currentPlaybackFrame.marbleAngularVelocity.clone(); + + this.getAbsPos().setPosition(expectedPos); + this.velocity = expectedVel; + this.setRotationQuat(this.level.replay.currentPlaybackFrame.marbleOrientation.clone()); + this.omega = expectedOmega; + } + if (this.controllable) { this.camera.update(timeState.currentAttemptTime, timeState.dt); } diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index c7efe6c8..d40fa024 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,6 @@ package src; +import src.Replay; import hxd.impl.Air3File.FileSeek; import gui.Canvas; import hxd.snd.Channel; @@ -121,14 +122,19 @@ class MarbleWorld extends Scheduler { var helpTextTimeState:Float = -1e8; var alertTextTimeState:Float = -1e8; + // Orientation var orientationChangeTime = -1e8; var oldOrientationQuat = new Quat(); - var resourceLoadFuncs:Array<(() -> Void)->Void> = []; - - /** The new target camera orientation quat */ public var newOrientationQuat = new Quat(); + // Replay + public var replay:Replay; + public var isWatching:Bool = false; + + // Loading + var resourceLoadFuncs:Array<(() -> Void)->Void> = []; + public var _disposed:Bool = false; public var _ready:Bool = false; @@ -146,6 +152,7 @@ class MarbleWorld extends Scheduler { this.scene = scene; this.scene2d = scene2d; this.mission = mission; + this.replay = new Replay(mission.path); } public function init() { @@ -313,6 +320,10 @@ class MarbleWorld extends Scheduler { } public function restart() { + if (!this.isWatching) + this.replay.clear(); + else + this.replay.rewind(); this.timeState.currentAttemptTime = 0; this.timeState.gameplayClock = 0; this.bonusTime = 0; @@ -762,6 +773,7 @@ class MarbleWorld extends Scheduler { } public function addDtsObject(obj:DtsObject, onFinish:Void->Void) { + obj.idInLevel = this.dtsObjects.length; // Set the id of the thing this.dtsObjects.push(obj); if (obj is ForceObject) { this.forceObjects.push(cast obj); @@ -807,8 +819,27 @@ class MarbleWorld extends Scheduler { if (!_ready) { return; } + if (!this.isWatching) + this.replay.startFrame(); + else { + if (!this.replay.advance(dt)) { + if (Util.isTouchDevice()) { + MarbleGame.instance.touchInput.hideControls(@:privateAccess this.playGui.playGuiCtrl); + } + this.setCursorLock(false); + this.dispose(); + var pmg = new PlayMissionGui(); + PlayMissionGui.currentSelectionStatic = mission.index + 1; + MarbleGame.canvas.setContent(pmg); + #if js + pointercontainer.hidden = false; + #end + } + } + ProfilerUI.measure("updateTimer"); this.updateTimer(dt); + this.tickSchedule(timeState.currentAttemptTime); this.updateGameState(); ProfilerUI.measure("updateDTS"); @@ -828,12 +859,20 @@ class MarbleWorld extends Scheduler { ProfilerUI.measure("updateAudio"); AudioManager.update(this.scene); + if (!this.isWatching) + this.replay.endFrame(); + if (this.outOfBounds && this.finishTime == null && Key.isDown(Settings.controlsSettings.powerup)) { this.clearSchedule(); this.restart(); return; } + if (Key.isDown(Key.H)) { + this.isWatching = true; + this.restart(); + } + this.updateTexts(); } @@ -875,32 +914,52 @@ class MarbleWorld extends Scheduler { public function updateTimer(dt:Float) { this.timeState.dt = dt; - if (this.bonusTime != 0 && this.timeState.currentAttemptTime >= 3.5) { - this.bonusTime -= dt; - if (this.bonusTime < 0) { - this.timeState.gameplayClock -= this.bonusTime; - this.bonusTime = 0; - } - if (timeTravelSound == null) { - var ttsnd = ResourceLoader.getResource("data/sound/timetravelactive.wav", ResourceLoader.getAudio, this.soundResources); - timeTravelSound = AudioManager.playSound(ttsnd, null, true); + if (!this.isWatching) { + if (this.bonusTime != 0 && this.timeState.currentAttemptTime >= 3.5) { + this.bonusTime -= dt; + if (this.bonusTime < 0) { + this.timeState.gameplayClock -= this.bonusTime; + this.bonusTime = 0; + } + if (timeTravelSound == null) { + var ttsnd = ResourceLoader.getResource("data/sound/timetravelactive.wav", ResourceLoader.getAudio, this.soundResources); + timeTravelSound = AudioManager.playSound(ttsnd, null, true); + } + } else { + if (timeTravelSound != null) { + timeTravelSound.stop(); + timeTravelSound = null; + } + if (this.timeState.currentAttemptTime >= 3.5) + this.timeState.gameplayClock += dt; + else if (this.timeState.currentAttemptTime + dt >= 3.5) { + this.timeState.gameplayClock += (this.timeState.currentAttemptTime + dt) - 3.5; + } } + this.timeState.currentAttemptTime += dt; } else { - if (timeTravelSound != null) { - timeTravelSound.stop(); - timeTravelSound = null; - } - if (this.timeState.currentAttemptTime >= 3.5) - this.timeState.gameplayClock += dt; - else if (this.timeState.currentAttemptTime + dt >= 3.5) { - this.timeState.gameplayClock += (this.timeState.currentAttemptTime + dt) - 3.5; + this.timeState.currentAttemptTime = this.replay.currentPlaybackFrame.time; + this.timeState.gameplayClock = this.replay.currentPlaybackFrame.clockTime; + this.bonusTime = this.replay.currentPlaybackFrame.bonusTime; + if (this.bonusTime != 0 && this.timeState.currentAttemptTime >= 3.5) { + if (timeTravelSound == null) { + var ttsnd = ResourceLoader.getResource("data/sound/timetravelactive.wav", ResourceLoader.getAudio, this.soundResources); + timeTravelSound = AudioManager.playSound(ttsnd, null, true); + } + } else { + if (timeTravelSound != null) { + timeTravelSound.stop(); + timeTravelSound = null; + } } } - this.timeState.currentAttemptTime += dt; this.timeState.timeSinceLoad += dt; if (finishTime != null) this.timeState.gameplayClock = finishTime.gameplayClock; playGui.formatTimer(this.timeState.gameplayClock); + + if (!this.isWatching) + this.replay.recordTimeState(timeState.currentAttemptTime, timeState.gameplayClock, this.bonusTime); } function updateTexts() { diff --git a/src/Replay.hx b/src/Replay.hx new file mode 100644 index 00000000..697af383 --- /dev/null +++ b/src/Replay.hx @@ -0,0 +1,191 @@ +package src; + +import haxe.EnumFlags; +import h3d.Quat; +import h3d.Vector; +import src.Util; + +enum ReplayMarbleState { + UsedPowerup; + Jumped; + InstantTeleport; +} + +@:publicFields +class ReplayFrame { + // Time + var time:Float; + var clockTime:Float; + var bonusTime:Float; + // Marble + var marblePosition:Vector; + var marbleVelocity:Vector; + var marbleOrientation:Quat; + var marbleAngularVelocity:Vector; + var marbleStateFlags:EnumFlags; + // Camera + var cameraPitch:Float; + var cameraYaw:Float; + // Input + var marbleX:Float; + var marbleY:Float; + + public function new() {} + + public function interpolate(next:ReplayFrame, time:Float) { + var t = (time - this.time) / (next.time - this.time); + + var dt = time - this.time; + + var interpFrame = new ReplayFrame(); + + // Interpolate time + interpFrame.time = time; + interpFrame.bonusTime = this.bonusTime; + interpFrame.clockTime = this.clockTime; + if (interpFrame.bonusTime != 0 && time >= 3.5) { + if (dt <= this.bonusTime) { + interpFrame.bonusTime -= dt; + } else { + interpFrame.clockTime += dt - this.bonusTime; + interpFrame.bonusTime = 0; + } + } else { + if (this.time >= 3.5) + interpFrame.clockTime += dt; + else if (this.time + dt >= 3.5) { + interpFrame.clockTime += (this.time + dt) - 3.5; + } + } + + // Interpolate marble + if (this.marbleStateFlags.has(InstantTeleport)) { + interpFrame.marblePosition = this.marblePosition.clone(); + interpFrame.marbleVelocity = this.marbleVelocity.clone(); + interpFrame.marbleOrientation = this.marbleOrientation.clone(); + interpFrame.marbleAngularVelocity = this.marbleAngularVelocity.clone(); + interpFrame.marbleStateFlags.set(InstantTeleport); + } else { + interpFrame.marblePosition = Util.lerpThreeVectors(this.marblePosition, next.marblePosition, t); + interpFrame.marbleVelocity = Util.lerpThreeVectors(this.marbleVelocity, next.marbleVelocity, t); + interpFrame.marbleOrientation = new Quat(); + interpFrame.marbleOrientation.slerp(this.marbleOrientation, next.marbleOrientation, t); + interpFrame.marbleAngularVelocity = Util.lerpThreeVectors(this.marbleAngularVelocity, next.marbleAngularVelocity, t); + } + + // Interpolate camera + if (this.marbleStateFlags.has(InstantTeleport)) { + interpFrame.cameraYaw = this.cameraYaw; + interpFrame.cameraPitch = this.cameraPitch; + } else { + interpFrame.cameraYaw = Util.lerp(this.cameraYaw, next.cameraYaw, t); + interpFrame.cameraPitch = Util.lerp(this.cameraPitch, next.cameraPitch, t); + } + + // State flags + if (this.marbleStateFlags.has(UsedPowerup)) + interpFrame.marbleStateFlags.set(UsedPowerup); + if (this.marbleStateFlags.has(Jumped)) + interpFrame.marbleStateFlags.set(Jumped); + + // Input + interpFrame.marbleX = this.marbleX; + interpFrame.marbleY = this.marbleY; + + return interpFrame; + } +} + +class Replay { + public var mission:String; + + var frames:Array; + + var currentRecordFrame:ReplayFrame; + + public var currentPlaybackFrame:ReplayFrame; + + var currentPlaybackFrameIdx:Int; + var currentPlaybackTime:Float; + + public function new(mission:String) { + this.mission = mission; + } + + public function startFrame() { + currentRecordFrame = new ReplayFrame(); + } + + public function endFrame() { + frames.push(currentRecordFrame); + currentRecordFrame = null; + } + + public function recordTimeState(time:Float, clockTime:Float, bonusTime:Float) { + currentRecordFrame.time = time; + currentRecordFrame.clockTime = clockTime; + currentRecordFrame.bonusTime = bonusTime; + } + + public function recordMarbleState(position:Vector, velocity:Vector, orientation:Quat, angularVelocity:Vector) { + currentRecordFrame.marblePosition = position.clone(); + currentRecordFrame.marbleVelocity = velocity.clone(); + currentRecordFrame.marbleOrientation = orientation.clone(); + currentRecordFrame.marbleAngularVelocity = angularVelocity.clone(); + } + + public function recordMarbleStateFlags(jumped:Bool, usedPowerup:Bool, instantTeleport:Bool) { + if (jumped) + currentRecordFrame.marbleStateFlags.set(Jumped); + if (usedPowerup) + currentRecordFrame.marbleStateFlags.set(UsedPowerup); + if (instantTeleport) + currentRecordFrame.marbleStateFlags.set(InstantTeleport); + } + + public function recordMarbleInput(x:Float, y:Float) { + currentRecordFrame.marbleX = x; + currentRecordFrame.marbleY = y; + } + + public function recordCameraState(pitch:Float, yaw:Float) { + currentRecordFrame.cameraPitch = pitch; + currentRecordFrame.cameraYaw = yaw; + } + + public function clear() { + this.frames = []; + currentRecordFrame = null; + } + + public function advance(dt:Float) { + if (this.currentPlaybackFrame == null) { + this.currentPlaybackFrame = this.frames[this.currentPlaybackFrameIdx]; + } + + var nextT = this.currentPlaybackTime + dt; + var startFrame = this.frames[this.currentPlaybackFrameIdx]; + if (this.currentPlaybackFrameIdx + 1 >= this.frames.length) { + return false; + } + var nextFrame = this.frames[this.currentPlaybackFrameIdx + 1]; + while (nextFrame.time <= nextT) { + this.currentPlaybackFrameIdx++; + if (this.currentPlaybackFrameIdx + 1 >= this.frames.length) { + return false; + } + var testNextFrame = this.frames[this.currentPlaybackFrameIdx + 1]; + startFrame = nextFrame; + nextFrame = testNextFrame; + } + this.currentPlaybackFrame = startFrame.interpolate(nextFrame, nextT); + this.currentPlaybackTime += dt; + return true; + } + + public function rewind() { + this.currentPlaybackTime = 0; + this.currentPlaybackFrame = null; + this.currentPlaybackFrameIdx = 0; + } +}