make client predict other marbles too

This commit is contained in:
RandomityGuy 2024-01-25 23:41:13 +05:30
parent 5ab1bb7a65
commit 1fef9a5bdc
6 changed files with 368 additions and 53 deletions

View file

@ -21,6 +21,7 @@ class SignallingHandler extends WebSocketHandler {
case StrMessage(content):
var conts = Json.parse(content);
if (conts.type == "connect") {
trace('Connect received');
var other = clients.find(x -> x != this);
other.send(Json.stringify(conts.sdpObj));
}

View file

@ -1,5 +1,7 @@
package src;
import net.MoveManager;
import net.MoveManager.NetMove;
import shaders.marble.CrystalMarb;
import shaders.marble.ClassicMarb;
import shapes.HelicopterImage;
@ -286,6 +288,8 @@ class Marble extends GameObject {
public var cubemapRenderer:CubemapRenderer;
var connection:net.Net.ClientConnection;
var moveMotionDir:Vector;
var isNetUpdate:Bool = false;
public function new() {
super();
@ -529,7 +533,7 @@ class Marble extends GameObject {
var motiondir = new Vector(0, -1, 0);
if (level.isReplayingMovement)
return level.currentInputMoves[1].marbleAxes;
if (this.controllable) {
if (this.controllable && !this.isNetUpdate) {
motiondir.transform(Matrix.R(0, 0, camera.CameraYaw));
motiondir.transform(level.newOrientationQuat.toMatrix());
var updir = this.level.currentUp;
@ -539,7 +543,11 @@ class Marble extends GameObject {
motiondir = updir.cross(sidedir);
return [sidedir, motiondir, updir];
} else {
return [new Vector(1, 0, 0), motiondir, new Vector(0, 0, 1)];
if (moveMotionDir != null)
motiondir = moveMotionDir;
var updir = this.level.currentUp;
var sidedir = motiondir.cross(updir);
return [sidedir, motiondir, updir];
}
}
@ -568,7 +576,7 @@ class Marble extends GameObject {
for (contact in contacts) {
if (contact.force != 0 && !forceObjects.contains(contact.otherObject)) {
if (contact.otherObject is RoundBumper) {
if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav")) {
if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav") && !this.isNetUpdate) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/bumperding1.wav", ResourceLoader.getAudio, this.soundResources));
playedSounds.push("data/sound/bumperding1.wav");
}
@ -808,7 +816,10 @@ class Marble extends GameObject {
}
if (sv < this._jumpImpulse) {
this.velocity.load(this.velocity.add(bestContact.normal.multiply((this._jumpImpulse - sv))));
if (!level.isReplayingMovement && !playedSounds.contains("data/sound/jump.wav")) {
if (!level.isReplayingMovement
&& !playedSounds.contains("data/sound/jump.wav")
&& !this.isNetUpdate
&& this.controllable) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/jump.wav", ResourceLoader.getAudio, this.soundResources));
playedSounds.push("data/sound/jump.wav");
}
@ -888,7 +899,7 @@ class Marble extends GameObject {
}
function bounceEmitter(speed:Float, normal:Vector) {
if (!this.controllable || level.isReplayingMovement)
if (!this.controllable || level.isReplayingMovement || this.isNetUpdate)
return;
if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) {
this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData,
@ -923,7 +934,7 @@ class Marble extends GameObject {
}
function playBoundSound(time:Float, contactVel:Float) {
if (level.isReplayingMovement)
if (level.isReplayingMovement || this.isNetUpdate)
return;
if (minVelocityBounceSoft <= contactVel) {
var hardBounceSpeed = minVelocityBounceHard;
@ -1607,20 +1618,102 @@ class Marble extends GameObject {
}
// MP Only Functions
public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array<PathedInterior>) {
var move:Move = null;
if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) {
move = recordMove();
public function packUpdate(move:NetMove) {
var b = new haxe.io.BytesOutput();
b.writeByte(NetPacketType.MarbleUpdate);
b.writeUInt16(connection != null ? connection.id : 0);
MoveManager.packMove(move, b);
b.writeUInt16(this.level.ticks); // So we can get the clients to do stuff about it
b.writeFloat(this.newPos.x);
b.writeFloat(this.newPos.y);
b.writeFloat(this.newPos.z);
b.writeFloat(this.velocity.x);
b.writeFloat(this.velocity.y);
b.writeFloat(this.velocity.z);
b.writeFloat(this.omega.x);
b.writeFloat(this.omega.y);
b.writeFloat(this.omega.z);
return b.getBytes();
}
if (!this.controllable && this.connection != null) {
move = new Move();
move.d = new Vector(0, 0);
public function unpackUpdate(b:haxe.io.BytesInput) {
// Assume packet header is already read
var serverMove = MoveManager.unpackMove(b);
if (Net.isClient)
Net.clientConnection.moveManager.acknowledgeMove(serverMove.id);
var serverTicks = b.readUInt16();
this.oldPos = this.newPos;
this.newPos = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
this.collider.transform.setPosition(this.newPos);
this.velocity = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
this.omega = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
// Apply the moves we have queued
if (Net.isClient) {
this.isNetUpdate = true;
if (this.controllable) {
for (move in @:privateAccess Net.clientConnection.moveManager.queuedMoves) {
moveMotionDir = move.motionDir;
advancePhysics(move.timeState, move.move, this.level.collisionWorld, this.level.pathedInteriors);
}
} else {
var tickDiff = this.level.ticks - serverTicks;
if (tickDiff > 0) {
var timeState = this.level.timeState.clone();
timeState.dt = 0.032;
var m = serverMove.move;
moveMotionDir = serverMove.motionDir;
for (o in 0...tickDiff) {
advancePhysics(timeState, m, this.level.collisionWorld, this.level.pathedInteriors);
}
}
}
this.isNetUpdate = false;
}
}
public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array<PathedInterior>, packets:Array<haxe.io.Bytes>) {
var move:NetMove = null;
if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) {
if (Net.isClient) {
var axis = getMarbleAxis()[1];
move = Net.clientConnection.moveManager.recordMove(axis, timeState);
} else if (Net.isHost) {
var axis = getMarbleAxis()[1];
var innerMove = recordMove();
move = new NetMove(innerMove, axis, timeState, 65535);
}
}
var moveId = 65535;
if (!this.controllable && this.connection != null && Net.isHost) {
var nextMove = this.connection.moveManager.getNextMove();
if (nextMove == null) {
var axis = getMarbleAxis()[1];
var innerMove = new Move();
innerMove.d = new Vector(0, 0);
move = new NetMove(innerMove, axis, timeState, 65535);
} else {
move = nextMove;
moveMotionDir = nextMove.motionDir;
moveId = nextMove.id;
}
}
if (move == null) {
var axis = getMarbleAxis()[1];
var innerMove = new Move();
innerMove.d = new Vector(0, 0);
move = new NetMove(innerMove, axis, timeState, 65535);
}
playedSounds = [];
advancePhysics(timeState, move, collisionWorld, pathedInteriors);
advancePhysics(timeState, move.move, collisionWorld, pathedInteriors);
physicsAccumulator = 0;
if (Net.isHost) {
packets.push(packUpdate(move));
}
}
public function updateClient(timeState:TimeState, pathedInteriors:Array<PathedInterior>) {

View file

@ -1,5 +1,6 @@
package src;
import net.MoveManager;
import net.NetCommands;
import net.Net;
import net.Net.ClientConnection;
@ -195,6 +196,9 @@ class MarbleWorld extends Scheduler {
public var startRealTime:Float = 0;
public var multiplayerStarted:Bool = false;
public var ticks:Int = 0; // How many 32ms ticks have happened
var tickAccumulator:Float = 0.0;
var clientMarbles:Map<ClientConnection, Marble> = [];
@ -233,6 +237,10 @@ class MarbleWorld extends Scheduler {
this.rewindManager = new RewindManager(this);
this.inputRecorder = new InputRecorder(this);
this.isMultiplayer = multiplayer;
if (this.isMultiplayer) {
isRecording = false;
isWatching = false;
}
// Set the network RNG for hunt
if (isMultiplayer && gameMode is modes.HuntMode && Net.isHost) {
@ -1055,16 +1063,13 @@ class MarbleWorld extends Scheduler {
return;
}
if (Key.isPressed(Key.T)) {
rollback(0.4);
}
var realDt = dt;
if ((Key.isDown(Settings.controlsSettings.rewind)
|| MarbleGame.instance.touchInput.rewindButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.rewind))
&& Settings.optionsSettings.rewindEnabled
&& !this.isMultiplayer
&& !this.isWatching
&& this.finishTime == null) {
this.rewinding = true;
@ -1072,7 +1077,8 @@ class MarbleWorld extends Scheduler {
if ((Key.isReleased(Settings.controlsSettings.rewind)
|| !MarbleGame.instance.touchInput.rewindButton.pressed
|| Gamepad.isReleased(Settings.gamepadSettings.rewind))
&& this.rewinding) {
&& this.rewinding
&& !this.isMultiplayer) {
if (this.isRecording) {
this.replay.spliceReplay(timeState.currentAttemptTime);
}
@ -1207,10 +1213,36 @@ class MarbleWorld extends Scheduler {
}
ProfilerUI.measure("updateMarbles");
if (this.isMultiplayer) {
tickAccumulator += timeState.dt;
while (tickAccumulator >= 0.032) {
var fixedDt = timeState.clone();
fixedDt.dt = 0.032;
tickAccumulator -= 0.032;
var packets = [];
marble.updateServer(fixedDt, collisionWorld, pathedInteriors, packets);
for (client => marble in clientMarbles) {
marble.updateServer(fixedDt, collisionWorld, pathedInteriors, packets);
}
if (Net.isHost) {
for (client => marble in clientMarbles) { // Oh no!
for (packet in packets) {
client.datachannel.sendBytes(packet);
}
}
}
ticks++;
}
marble.updateClient(timeState, this.pathedInteriors);
for (client => marble in clientMarbles) {
marble.updateClient(timeState, this.pathedInteriors);
}
} else {
marble.update(timeState, collisionWorld, this.pathedInteriors);
for (client => marble in clientMarbles) {
marble.update(timeState, collisionWorld, this.pathedInteriors);
}
}
_cubemapNeedsUpdate = true;
Renderer.dirtyBuffers = true;
if (this.rewinding) {
@ -1243,7 +1275,7 @@ class MarbleWorld extends Scheduler {
}
}
if (!this.rewinding && Settings.optionsSettings.rewindEnabled)
if (!this.rewinding && Settings.optionsSettings.rewindEnabled && !this.isMultiplayer)
this.rewindManager.recordFrame();
if (!this.isReplayingMovement) {

View file

@ -89,7 +89,7 @@ class SphereCollisionEntity extends CollisionEntity {
contact.collider = this;
contact.friction = 1;
contact.restitution = 1;
contact.velocity = this.velocity;
contact.velocity = this.velocity.clone();
contact.otherObject = this.go;
contact.point = position.add(normDist);
contact.normal = normDist.multiply(-1);
@ -102,9 +102,9 @@ class SphereCollisionEntity extends CollisionEntity {
othercontact.collider = collisionEntity;
othercontact.friction = 1;
othercontact.restitution = 1;
othercontact.velocity = this.velocity;
othercontact.point = thispos.add(position).multiply(0.5);
othercontact.normal = contact.point.sub(position).normalized();
othercontact.velocity = collisionEntity.velocity.clone();
othercontact.point = thispos.sub(normDist);
othercontact.normal = normDist.clone();
othercontact.contactDistance = contact.point.distance(position);
othercontact.force = 0;
othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal));

157
src/net/MoveManager.hx Normal file
View file

@ -0,0 +1,157 @@
package net;
import src.TimeState;
import src.Console;
import net.Net.ClientConnection;
import net.Net.NetPacketType;
import src.MarbleWorld;
import src.Marble.Move;
import h3d.Vector;
import src.Gamepad;
import src.Settings;
import hxd.Key;
import src.MarbleGame;
import src.Util;
@:publicFields
class NetMove {
var motionDir:Vector;
var move:Move;
var id:Int;
var timeState:TimeState;
public function new(move:Move, motionDir:Vector, timeState:TimeState, id:Int) {
this.move = move;
this.motionDir = motionDir;
this.id = id;
this.timeState = timeState;
}
}
class MoveManager {
var connection:ClientConnection;
var queuedMoves:Array<NetMove>;
var nextMoveId:Int;
var lastMove:NetMove;
var lastAckMoveId:Int = -1;
static var maxMoves = 45; // Taken from Torque
public function new(connection:ClientConnection) {
queuedMoves = [];
nextMoveId = 0;
this.connection = connection;
}
public function recordMove(motionDir:Vector, timeState:TimeState) {
if (queuedMoves.length >= maxMoves)
return queuedMoves[queuedMoves.length - 1];
var move = new Move();
move.d = new Vector();
move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis);
move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis);
if (Key.isDown(Settings.controlsSettings.forward)) {
move.d.x -= 1;
}
if (Key.isDown(Settings.controlsSettings.backward)) {
move.d.x += 1;
}
if (Key.isDown(Settings.controlsSettings.left)) {
move.d.y += 1;
}
if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1;
}
if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) {
move.jump = true;
}
if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup))
|| (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.powerup)) {
move.powerup = true;
}
if (MarbleGame.instance.touchInput.movementInput.pressed) {
move.d.y = -MarbleGame.instance.touchInput.movementInput.value.x;
move.d.x = MarbleGame.instance.touchInput.movementInput.value.y;
}
var netMove = new NetMove(move, motionDir, timeState.clone(), nextMoveId++);
queuedMoves.push(netMove);
if (nextMoveId >= 65535) // 65535 is reserved for null move
nextMoveId = 0;
var b = new haxe.io.BytesOutput();
b.writeByte(NetPacketType.MarbleMove);
b.writeUInt16(Net.clientId);
Net.sendPacketToHost(packMove(netMove, b));
return netMove;
}
public static function packMove(m:NetMove, b:haxe.io.BytesOutput) {
b.writeUInt16(m.id);
b.writeFloat(m.move.d.x);
b.writeFloat(m.move.d.y);
var flags = 0;
if (m.move.jump)
flags |= 1;
if (m.move.powerup)
flags |= 2;
b.writeByte(flags);
b.writeFloat(m.motionDir.x);
b.writeFloat(m.motionDir.y);
b.writeFloat(m.motionDir.z);
return b;
}
public static function unpackMove(b:haxe.io.BytesInput) {
var moveId = b.readUInt16();
var move = new Move();
move.d = new Vector();
move.d.x = b.readFloat();
move.d.y = b.readFloat();
var flags = b.readByte();
move.jump = (flags & 1) != 0;
move.powerup = (flags & 2) != 0;
var motionDir = new Vector();
motionDir.x = b.readFloat();
motionDir.y = b.readFloat();
motionDir.z = b.readFloat();
var netMove = new NetMove(move, motionDir, MarbleGame.instance.world.timeState.clone(), moveId);
return netMove;
}
public function queueMove(m:NetMove) {
queuedMoves.push(m);
}
public function getNextMove() {
if (queuedMoves.length == 0)
return lastMove;
else {
lastMove = queuedMoves[0];
queuedMoves.shift();
return lastMove;
}
}
public function acknowledgeMove(m:Int) {
if (m == 65535 || m == -1)
return;
if (m <= lastAckMoveId)
return; // Already acked
if (queuedMoves.length == 0)
return;
while (m != queuedMoves[0].id) {
trace('Ignoring move ${queuedMoves[0].id}, need ${m}');
queuedMoves.shift();
}
if (m == queuedMoves[0].id)
queuedMoves.shift();
lastAckMoveId = m;
}
}

View file

@ -6,6 +6,8 @@ import datachannel.RTCDataChannel;
import hx.ws.WebSocket;
import src.Console;
import net.NetCommands;
import src.MarbleGame;
import hx.ws.Types.MessageType;
enum abstract GameplayState(Int) from Int to Int {
var UNKNOWN;
@ -19,6 +21,8 @@ enum abstract NetPacketType(Int) from Int to Int {
var NetCommand;
var Ping;
var PingBack;
var MarbleUpdate;
var MarbleMove;
}
@:publicFields
@ -27,6 +31,7 @@ class ClientConnection {
var socket:RTCPeerConnection;
var datachannel:RTCDataChannel;
var state:GameplayState;
var moveManager:MoveManager;
var rtt:Float;
var pingSendTime:Float;
var _rttRecords:Array<Float> = [];
@ -37,6 +42,7 @@ class ClientConnection {
this.id = id;
this.state = GameplayState.LOBBY;
this.rtt = 0;
this.moveManager = new MoveManager(this);
}
public function ready() {
@ -60,6 +66,7 @@ class Net {
public static var networkRNG:Float;
public static var clients:Map<RTCPeerConnection, ClientConnection> = [];
public static var clientIdMap:Map<Int, ClientConnection> = [];
public static var clientConnection:ClientConnection;
public static function hostServer() {
// host = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0");
@ -76,9 +83,18 @@ class Net {
switch (m) {
case StrMessage(content):
var conts = Json.parse(content);
var peer = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0");
var peer = new RTCPeerConnection(["stun:stun.l.google.com:19302"], "0.0.0.0");
peer.setRemoteDescription(conts.sdp, conts.type);
addClient(peer);
case BytesMessage(content): {}
}
}
isMP = true;
}
static function addClient(peer:RTCPeerConnection) {
var candidates = [];
peer.onLocalCandidate = (c) -> {
if (c != "")
@ -92,25 +108,20 @@ class Net {
type: "connect",
sdpObj: {
sdp: sdpObj,
type: "offer"
type: "answer"
}
}));
}
}
peer.onDataChannel = (dc) -> {
peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> {
onClientConnect(peer, dc);
};
case _: {}
}
}
isMP = true;
}
public static function joinServer(connectedCb:() -> Void) {
masterWs = new WebSocket("ws://localhost:8080");
client = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0");
client = new RTCPeerConnection(["stun:stun.l.google.com:19302"], "0.0.0.0");
var candidates = [];
client.onLocalCandidate = (c) -> {
@ -119,6 +130,7 @@ class Net {
}
client.onGatheringStateChange = (s) -> {
if (s == RTC_GATHERING_COMPLETE) {
Console.log("Local Description Set!");
var sdpObj = StringTools.trim(client.localDescription);
sdpObj = sdpObj + '\r\n' + candidates.join('\r\n');
masterWs.send(Json.stringify({
@ -134,6 +146,7 @@ class Net {
masterWs.onmessage = (m) -> {
switch (m) {
case StrMessage(content):
Console.log("Remote Description Received!");
var conts = Json.parse(content);
client.setRemoteDescription(conts.sdp, conts.type);
case _: {}
@ -142,8 +155,10 @@ class Net {
clientDatachannel = client.createDatachannel("mp");
clientDatachannel.onOpen = (n) -> {
Console.log("Successfully connected!");
clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0
clientIdMap[0] = clients[client];
clientConnection = clients[client];
onConnectedToServer();
haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation
}
@ -172,7 +187,7 @@ class Net {
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
clients[c].pingSendTime = Sys.time();
clients[c].pingSendTime = Console.time();
dc.sendBytes(b);
Console.log("Sending ping packet!");
}
@ -183,7 +198,7 @@ class Net {
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
clients[client].pingSendTime = Sys.time();
clients[client].pingSendTime = Console.time();
clientDatachannel.sendBytes(b);
Console.log("Sending ping packet!");
}
@ -210,7 +225,7 @@ class Net {
var pingLeft = input.readByte();
Console.log("Got pingback packet!");
var conn = clients[c];
var now = Sys.time();
var now = Console.time();
conn._rttRecords.push((now - conn.pingSendTime));
if (pingLeft > 0) {
conn.pingSendTime = now;
@ -225,6 +240,23 @@ class Net {
Console.log('Got RTT ${conn.rtt} for client ${conn.id}');
}
case MarbleUpdate:
var marbleClientId = input.readUInt16();
if (marbleClientId == clientId) {
if (MarbleGame.instance.world != null)
MarbleGame.instance.world.marble.unpackUpdate(input);
} else {
var cc = clientIdMap[marbleClientId];
if (MarbleGame.instance.world != null)
@:privateAccess MarbleGame.instance.world.clientMarbles[cc].unpackUpdate(input);
}
case MarbleMove:
var marbleClientId = input.readUInt16();
var cc = clientIdMap[marbleClientId];
var m = MoveManager.unpackMove(input);
cc.moveManager.queueMove(m);
case _:
trace("unknown command: " + packetType);
}