From 723c3f34f347a836d3beef338ef445f79666382c Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Thu, 27 Jun 2024 01:57:14 +0530 Subject: [PATCH] spectator mode --- src/CameraController.hx | 233 ++++++++++++++++++++++++++++++++++++ src/Marble.hx | 10 ++ src/MarbleWorld.hx | 1 + src/gui/MPPreGameDlg.hx | 18 ++- src/net/ClientConnection.hx | 6 + src/net/Net.hx | 11 ++ src/net/NetCommands.hx | 26 ++++ 7 files changed, 301 insertions(+), 4 deletions(-) diff --git a/src/CameraController.hx b/src/CameraController.hx index 44d885f4..bec342eb 100644 --- a/src/CameraController.hx +++ b/src/CameraController.hx @@ -1,5 +1,6 @@ package src; +import net.Net; import mis.MisParser; import h3d.col.Bounds; import h3d.col.Plane; @@ -28,6 +29,7 @@ import h3d.Vector; import hxsl.Types.Matrix; import h3d.scene.Scene; import src.Gamepad; +import src.MarbleGame; enum CameraMode { FreeOrbit; @@ -71,6 +73,9 @@ class CameraController extends Object { public var finish:Bool = false; public var overview:Bool = false; + var spectate:Bool = false; + var spectateMarbleIndex:Int = -1; + var overviewCenter:Vector; var overviewWidth:Vector; var overviewHeight:Float; @@ -124,6 +129,14 @@ class CameraController extends Object { #end } + public function enableSpectate() { + spectate = true; + } + + public function stopSpectate() { + spectate = false; + } + public function orbit(mouseX:Float, mouseY:Float, isTouch:Bool = false) { if (_ignoreCursor) { _ignoreCursor = false; @@ -226,6 +239,221 @@ class CameraController extends Object { camera.up.z = 1; } + function doSpectateCamera(currentTime:Float, dt:Float) { + var camera = level.scene.camera; + + var lerpt = Math.pow(0.5, dt / 0.032); // Math.min(1, 1 - Math.pow(0.6, dt / 0.032)); // hxd.Math.min(1, 1 - Math.pow(0.6, dt * 600)); + + var cameraPitchDelta = (Key.isDown(Settings.controlsSettings.camBackward) ? 1 : 0) + - (Key.isDown(Settings.controlsSettings.camForward) ? 1 : 0) + + Gamepad.getAxis(Settings.gamepadSettings.cameraYAxis); + if (Settings.gamepadSettings.invertYAxis) + cameraPitchDelta = -cameraPitchDelta; + nextCameraPitch += 0.75 * 5 * cameraPitchDelta * dt * Settings.gamepadSettings.cameraSensitivity; + var cameraYawDelta = (Key.isDown(Settings.controlsSettings.camRight) ? 1 : 0) - (Key.isDown(Settings.controlsSettings.camLeft) ? 1 : 0) + + Gamepad.getAxis(Settings.gamepadSettings.cameraXAxis); + if (Settings.gamepadSettings.invertXAxis) + cameraYawDelta = -cameraYawDelta; + nextCameraYaw += 0.75 * 5 * cameraYawDelta * dt * Settings.gamepadSettings.cameraSensitivity; + + nextCameraPitch = Math.max(-Math.PI / 2 + Math.PI / 4, Math.min(Math.PI / 2 - 0.0001, nextCameraPitch)); + + CameraYaw = Util.lerp(CameraYaw, nextCameraYaw, lerpt); + CameraPitch = Util.lerp(CameraPitch, nextCameraPitch, lerpt); + + CameraPitch = Math.max(-Math.PI / 2 + Math.PI / 4, Math.min(Math.PI / 2 - 0.0001, CameraPitch)); // Util.clamp(CameraPitch, -Math.PI / 12, Math.PI / 2); + + function getRotQuat(v1:Vector, v2:Vector) { + function orthogonal(v:Vector) { + var x = Math.abs(v.x); + var y = Math.abs(v.y); + var z = Math.abs(v.z); + var other = x < y ? (x < z ? new Vector(1, 0, 0) : new Vector(0, 0, 1)) : (y < z ? new Vector(0, 1, 0) : new Vector(0, 0, 1)); + return v.cross(other); + } + + var u = v1.normalized(); + var v = v2.normalized(); + if (u.multiply(-1).equals(v)) { + var q = new Quat(); + var o = orthogonal(u).normalized(); + q.x = o.x; + q.y = o.y; + q.z = o.z; + q.w = 0; + return q; + } + var half = u.add(v).normalized(); + var q = new Quat(); + q.w = u.dot(half); + var vr = u.cross(half); + q.x = vr.x; + q.y = vr.y; + q.z = vr.z; + return q; + } + var orientationQuat = level.getOrientationQuat(currentTime); + + if (spectateMarbleIndex == -1) { + var up = new Vector(0, 0, 1); + up.transform(orientationQuat.toMatrix()); + var directionVector = new Vector(1, 0, 0); + + var q1 = new Quat(); + q1.initRotateAxis(0, 1, 0, CameraPitch); + directionVector.transform(q1.toMatrix()); + q1.initRotateAxis(0, 0, 1, CameraYaw); + directionVector.transform(q1.toMatrix()); + directionVector.transform(orientationQuat.toMatrix()); + + var dy = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis) * CameraSpeed * dt; + var dx = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis) * CameraSpeed * dt; + + if (Key.isDown(Settings.controlsSettings.forward)) { + dy += CameraSpeed * dt; + } + if (Key.isDown(Settings.controlsSettings.backward)) { + dy -= CameraSpeed * dt; + } + if (Key.isDown(Settings.controlsSettings.left)) { + dx += CameraSpeed * dt; + } + if (Key.isDown(Settings.controlsSettings.right)) { + dx -= CameraSpeed * dt; + } + + if (MarbleGame.instance.touchInput.movementInput.pressed) { + dx = -MarbleGame.instance.touchInput.movementInput.value.x * CameraSpeed * dt; + dy = MarbleGame.instance.touchInput.movementInput.value.y * CameraSpeed * dt; + } + + if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup)) + || (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed) + || Gamepad.isDown(Settings.gamepadSettings.powerup)) { + dx *= 2; + dy *= 2; + } + + if (Key.isPressed(Settings.controlsSettings.blast) + || (MarbleGame.instance.touchInput.blastbutton.pressed) + || Gamepad.isPressed(Settings.gamepadSettings.blast)) { + var freeMarbleIndex = -1; + + for (i in 0...level.marbles.length) { + var marble = level.marbles[i]; + @:privateAccess if ((marble.connection != null && !marble.connection.spectator)) { + freeMarbleIndex = i; + break; + } + } + spectateMarbleIndex = freeMarbleIndex; + return; + } + + var sideDir = directionVector.cross(up); + + var moveDir = directionVector.multiply(dy).add(sideDir.multiply(dx)); + camera.pos.load(camera.pos.add(moveDir)); + + camera.up = up; + camera.target = camera.pos.add(directionVector); + } else { + if (Key.isPressed(Settings.controlsSettings.left)) { + spectateMarbleIndex = (spectateMarbleIndex - 1 + level.marbles.length) % level.marbles.length; + @:privateAccess while (level.marbles[spectateMarbleIndex].connection == null + || level.marbles[spectateMarbleIndex].connection.spectator) { + spectateMarbleIndex = (spectateMarbleIndex - 1 + level.marbles.length) % level.marbles.length; + } + } + + if (Key.isPressed(Settings.controlsSettings.right)) { + spectateMarbleIndex = (spectateMarbleIndex + 1 + level.marbles.length) % level.marbles.length; + @:privateAccess while (level.marbles[spectateMarbleIndex].connection == null + || level.marbles[spectateMarbleIndex].connection.spectator) { + spectateMarbleIndex = (spectateMarbleIndex + 1 + level.marbles.length) % level.marbles.length; + } + } + + if (Key.isPressed(Settings.controlsSettings.blast) + || (MarbleGame.instance.touchInput.blastbutton.pressed) + || Gamepad.isPressed(Settings.gamepadSettings.blast)) { + spectateMarbleIndex = -1; + return; + } + + var marblePosition = level.marbles[spectateMarbleIndex].getAbsPos().getPosition(); + var up = new Vector(0, 0, 1); + up.transform(orientationQuat.toMatrix()); + var directionVector = new Vector(1, 0, 0); + var cameraVerticalTranslation = new Vector(0, 0, 0.3); + + var q1 = new Quat(); + q1.initRotateAxis(0, 1, 0, CameraPitch); + directionVector.transform(q1.toMatrix()); + cameraVerticalTranslation.transform(q1.toMatrix()); + q1.initRotateAxis(0, 0, 1, CameraYaw); + directionVector.transform(q1.toMatrix()); + cameraVerticalTranslation.transform(q1.toMatrix()); + directionVector.transform(orientationQuat.toMatrix()); + cameraVerticalTranslation.transform(orientationQuat.toMatrix()); + camera.up = up; + camera.pos = marblePosition.sub(directionVector.multiply(CameraDistance)); + camera.target = marblePosition.add(cameraVerticalTranslation); + + var closeness = 0.1; + var rayCastOrigin = marblePosition.add(level.marbles[spectateMarbleIndex].currentUp.multiply(marble._radius)); + + var processedShapes = []; + for (i in 0...3) { + var rayCastDirection = camera.pos.sub(rayCastOrigin); + rayCastDirection = rayCastDirection.add(rayCastDirection.normalized().multiply(2)); + + var rayCastLen = rayCastDirection.length(); + + var results = level.collisionWorld.rayCast(rayCastOrigin, rayCastDirection.normalized(), rayCastLen); + + var firstHit:octree.IOctreeObject.RayIntersectionData = null; + var firstHitDistance = 1e8; + for (result in results) { + if (!processedShapes.contains(result.object) + && (firstHit == null || (rayCastOrigin.distance(result.point) < firstHitDistance))) { + firstHit = result; + firstHitDistance = rayCastOrigin.distance(result.point); + processedShapes.push(result.object); + } + } + + if (firstHit != null) { + if (firstHitDistance < CameraDistance) { + // camera.pos = marblePosition.sub(directionVector.multiply(firstHit.distance * 0.7)); + var plane = new Plane(firstHit.normal.x, firstHit.normal.y, firstHit.normal.z, firstHit.point.dot(firstHit.normal)); + var normal = firstHit.normal.multiply(-1); + // var position = firstHit.point; + + var projected = plane.project(camera.pos.toPoint()); + var dist = plane.distance(camera.pos.toPoint()); + + if (dist >= closeness) + break; + + camera.pos = projected.toVector().add(normal.multiply(-closeness)); + + var forwardVec = marblePosition.sub(camera.pos).normalized(); + var rightVec = camera.up.cross(forwardVec).normalized(); + var upVec = forwardVec.cross(rightVec); + + camera.target = marblePosition.add(upVec.multiply(0.3)); + camera.up = upVec; + continue; + } + } + break; + } + } + + this.setPosition(camera.pos.x, camera.pos.y, camera.pos.z); + } + public function update(currentTime:Float, dt:Float) { // camera.position.set(marblePosition.x, marblePosition.y, marblePosition.z).sub(directionVector.clone().multiplyScalar(2.5)); // this.level.scene.camera.target = marblePosition.add(cameraVerticalTranslation); @@ -236,6 +464,11 @@ class CameraController extends Object { return; } + if (spectate) { + doSpectateCamera(currentTime, dt); + return; + } + var camera = level.scene.camera; var lerpt = hxd.Math.min(1, 1 - Math.pow(0.6, dt * 600)); diff --git a/src/Marble.hx b/src/Marble.hx index fd033963..0cf0e41d 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1641,6 +1641,16 @@ class Marble extends GameObject { oldPos = this.collider.transform.getPosition(); prevRot = this.getRotationQuat().clone(); + // Handle spectator hacky bullshit + if (Net.isMP) { + if ((connection != null && connection.spectator) || (connection == null && (Net.hostSpectate || Net.clientSpectate))) { + this.collider.transform.setPosition(new Vector(1e8, 1e8, 1e8)); + this.collisionWorld.updateTransform(this.collider); + this.setPosition(1e8, 1e8, 1e8); + return; + } + } + // if (this.controllable) { for (interior in pathedInteriors) { if (Net.isMP) diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 5cf55411..c176e60d 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -540,6 +540,7 @@ class MarbleWorld extends Scheduler { public function start() { Console.log("LEVEL START"); restart(this.marble, true); + for (interior in this.interiors) interior.onLevelStart(); for (shape in this.dtsObjects) diff --git a/src/gui/MPPreGameDlg.hx b/src/gui/MPPreGameDlg.hx index 28af5676..bfef6360 100644 --- a/src/gui/MPPreGameDlg.hx +++ b/src/gui/MPPreGameDlg.hx @@ -107,6 +107,11 @@ class MPPreGameDlg extends GuiControl { spectateBtn.vertSizing = Top; spectateBtn.position = new Vector(190, 394); spectateBtn.extent = new Vector(127, 33); + spectateBtn.buttonType = Toggle; + spectateBtn.pressedAction = (e) -> { + NetCommands.toggleSpectate(Net.isHost ? 0 : Net.clientId); + updatePlayerList(); + } dialogImg.addChild(spectateBtn); var serverTitle = new GuiText(markerFelt24); @@ -223,20 +228,23 @@ class MPPreGameDlg extends GuiControl { if (Net.isHost) { playerListArr.push({ name: Settings.highscoreName, - ready: Net.lobbyHostReady + ready: Net.lobbyHostReady, + spectate: Net.hostSpectate }); } if (Net.isClient) { playerListArr.push({ name: Settings.highscoreName, - ready: Net.lobbyClientReady + ready: Net.lobbyClientReady, + spectate: Net.clientSpectate }); } if (Net.clientIdMap != null) { for (c => v in Net.clientIdMap) { playerListArr.push({ name: v.name, - ready: v.lobbyReady + ready: v.lobbyReady, + spectate: v.spectator }); } } @@ -249,7 +257,7 @@ class MPPreGameDlg extends GuiControl { playBtn.disabled = !allReady; - var playerListCompiled = playerListArr.map(player -> player.name); + var playerListCompiled = playerListArr.map(player -> player.spectate ? '[S] ${player.name}' : player.name); var playerListStateCompiled = playerListArr.map(player -> player.ready ? "[Ready]" : "[Waiting]"); playerListLeft.setTexts(playerListCompiled); playerListRight.setTexts(playerListStateCompiled); @@ -268,7 +276,9 @@ class MPPreGameDlg extends GuiControl { // Make everyone un-lobby ready (again!) for (c in Net.clients) { c.lobbyReady = false; + c.spectator = false; } + Net.hostSpectate = false; Net.lobbyClientReady = false; Net.lobbyHostReady = false; if (Net.isHost) { diff --git a/src/net/ClientConnection.hx b/src/net/ClientConnection.hx index e89d3485..a9ad1e08 100644 --- a/src/net/ClientConnection.hx +++ b/src/net/ClientConnection.hx @@ -77,11 +77,13 @@ abstract class GameConnection { var platform:NetPlatform; var marbleId:Int; var marbleCatId:Int; + var spectator:Bool; function new(id:Int) { this.id = id; this.moveManager = new MoveManager(this); this.lobbyReady = false; + this.spectator = false; } public function ready() { @@ -92,6 +94,10 @@ abstract class GameConnection { lobbyReady = !lobbyReady; } + public function toggleSpectate() { + spectator = !spectator; + } + public function queueMove(m:NetMove) { moveManager.queueMove(m); } diff --git a/src/net/Net.hx b/src/net/Net.hx index 28e21355..0a915acf 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -85,6 +85,8 @@ class Net { public static var lobbyHostReady:Bool; public static var lobbyClientReady:Bool; public static var hostReady:Bool; + public static var hostSpectate:Bool; + public static var clientSpectate:Bool; static var clientIdAllocs:Int = 1; public static var clientId:Int; @@ -373,6 +375,8 @@ class Net { Net.lobbyHostReady = false; Net.lobbyClientReady = false; Net.hostReady = false; + Net.hostSpectate = false; + Net.clientSpectate = false; // MultiplayerLevelSelectGui.custSelected = false; } if (Net.isHost) { @@ -394,6 +398,8 @@ class Net { Net.lobbyHostReady = false; Net.lobbyClientReady = false; Net.hostReady = false; + Net.hostSpectate = false; + Net.clientSpectate = false; // MultiplayerLevelSelectGui.custSelected = false; } } @@ -629,6 +635,7 @@ class Net { b.writeByte(v.platform); b.writeByte(v.marbleId); b.writeByte(v.marbleCatId); + b.writeByte(v.spectator ? 1 : 0); var name = v.getName(); b.writeByte(name.length); for (i in 0...name.length) { @@ -641,6 +648,7 @@ class Net { b.writeByte(getPlatform()); b.writeByte(Settings.optionsSettings.marbleIndex); b.writeByte(Settings.optionsSettings.marbleCategoryIndex); + b.writeByte(Net.hostSpectate ? 1 : 0); var name = Settings.highscoreName; b.writeByte(name.length); for (i in 0...name.length) { @@ -752,6 +760,7 @@ class Net { var platform = input.readByte(); var marble = input.readByte(); var marbleCat = input.readByte(); + var cspectator = input.readByte() == 1; if (id != 0 && id != Net.clientId && !clientIdMap.exists(id)) { Console.log('Adding ghost connection ${id}'); addGhost(id); @@ -767,9 +776,11 @@ class Net { clientIdMap[id].setMarbleId(marble, marbleCat); clientIdMap[id].lobbyReady = cready; clientIdMap[id].platform = platform; + clientIdMap[id].spectator = cspectator; } if (Net.clientId == id) { Net.lobbyClientReady = cready; + Net.clientSpectate = cspectator; } } if (MarbleGame.canvas.content is MPPlayMissionGui) { diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx index 28410e5e..858957d4 100644 --- a/src/net/NetCommands.hx +++ b/src/net/NetCommands.hx @@ -132,6 +132,26 @@ class NetCommands { } } + @:rpc(client) public static function toggleSpectate(clientId:Int) { + if (Net.isHost) { + if (clientId == 0) + Net.hostSpectate = !Net.hostSpectate; + else + Net.clientIdMap[clientId].toggleSpectate(); + + if (MarbleGame.canvas.content is MPPlayMissionGui) { + cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames(); + } + if (MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1] is MPPreGameDlg) { + cast(MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1], MPPreGameDlg).updatePlayerList(); + } + var b = Net.sendPlayerInfosBytes(); + for (cc in Net.clients) { + cc.sendBytes(b); + } + } + } + @:rpc(client) public static function clientIsReady(clientId:Int) { if (Net.isHost) { if (Net.serverInfo.state == "WAITING") { @@ -214,6 +234,12 @@ class NetCommands { MarbleGame.instance.world.setCursorLock(true); MarbleGame.instance.world.marble.camera.stopOverview(); } + + if (Net.clientSpectate || Net.hostSpectate) { + MarbleGame.instance.world.marble.camera.enableSpectate(); + } else { + MarbleGame.instance.world.marble.camera.stopSpectate(); + } } }