diff --git a/src/Marble.hx b/src/Marble.hx index f244a1e9..ee8d19b3 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1062,7 +1062,7 @@ class Marble extends GameObject { searchbox.addSpherePos(position.x + velocity.x * deltaT, position.y + velocity.y * deltaT, position.z + velocity.z * deltaT, _radius); var foundObjs = this.collisionWorld.boundingSearch(searchbox); - foundObjs.push(this.collisionWorld.staticWorld); + // foundObjs.push(this.collisionWorld.staticWorld); var finalT = deltaT; var found = false; @@ -1151,7 +1151,7 @@ class Marble extends GameObject { // var v = surface.points[surface.indices[i + 1]].transformed(obj.transform); // var v2 = surface.points[surface.indices[i + 2]].transformed(obj.transform); - var triangleVerts = [v0, v, v2]; + // var triangleVerts = [v0, v, v2]; var surfaceNormal = new Vector(verts.nx, verts.ny, verts.nz); // surface.normals[surface.indices[i]].transformed3x3(obj.transform).normalized(); @@ -1166,8 +1166,8 @@ class Marble extends GameObject { } testTriangles.push({ - v: [v0, v, v2], - n: surfaceNormal, + v: [v0.clone(), v.clone(), v2.clone()], + n: surfaceNormal.clone(), }); // Time until collision with the plane @@ -1190,7 +1190,9 @@ class Marble extends GameObject { } // We *might* be colliding with an edge - var lastVert = v2; + var triangleVerts = [v0.clone(), v.clone(), v2.clone()]; + + var lastVert = v2.clone(); var radSq = radius * radius; for (iter in 0...3) { @@ -1211,7 +1213,7 @@ class Marble extends GameObject { // If it's not quadratic or has no solution, ignore this edge. if (a == 0.0 || discriminant < 0.0) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1231,7 +1233,7 @@ class Marble extends GameObject { // If the collision doesn't happen on this time step, ignore this edge. if (edgeCollisionTime2 <= 0.0001 || finalT <= edgeCollisionTime) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1245,7 +1247,7 @@ class Marble extends GameObject { // If the collision happens outside the boundaries of the edge, ignore this edge. if (-radius > distanceAlongEdge || edgeLen + radius < distanceAlongEdge) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1309,14 +1311,14 @@ class Marble extends GameObject { // We still need to check the other corner ... // Build one last quadratic equation to solve for the collision time - posVertDiff = position.sub(lastVert); + var posVertDiff = position.sub(lastVert); b = 2 * posVertDiff.dot(relVel); c = posVertDiff.lengthSq() - radSq; discriminant = b * b - (4 * a * c); // If it's not quadratic or has no solution, then skip this corner if (a == 0.0 || discriminant < 0.0) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1335,7 +1337,7 @@ class Marble extends GameObject { } if (edgeCollisionTime2 <= 0.0001 || finalT <= edgeCollisionTime) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1343,7 +1345,7 @@ class Marble extends GameObject { edgeCollisionTime = 0; if (edgeCollisionTime < 0.000001) { - lastVert = thisVert; + lastVert.load(thisVert); continue; } @@ -1351,7 +1353,7 @@ class Marble extends GameObject { currentFinalPos = position.add(relVel.multiply(finalT)); // Debug.drawSphere(currentFinalPos, radius); - lastVert = thisVert; + lastVert.load(thisVert); found = true; // iterationFound = true; } @@ -1415,7 +1417,7 @@ class Marble extends GameObject { // Nudge to the surface of the contact plane Debug.drawTriangle(testTri.v[0], testTri.v[1], testTri.v[2]); Debug.drawSphere(position, radius); - position = position.add(separatingDistance.multiply(radius - distToContactPlane - 0.005)); + position.load(position.add(separatingDistance.multiply(radius - distToContactPlane - 0.005))); resolved++; } } @@ -1446,7 +1448,7 @@ class Marble extends GameObject { var dist = marblePosition.distance(position); if (dist < radius + marble.radius + 0.001) { var separatingDistance = position.sub(marblePosition).normalized(); - position = position.add(separatingDistance.multiply(radius + marble.radius + 0.001 - dist)); + position.load(position.add(separatingDistance.multiply(radius + marble.radius + 0.001 - dist))); } } return position; diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 9c8e509b..ee48ccaf 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,7 @@ package src; +import net.MarblePredictionStore; +import net.MarblePredictionStore.MarblePrediction; import net.MarbleUpdateQueue; import haxe.Exception; import net.NetPacket.MarbleUpdatePacket; @@ -103,6 +105,7 @@ import modes.NullMode; import modes.GameMode.GameModeFactory; import src.Renderer; import src.Analytics; +import src.Debug; class MarbleWorld extends Scheduler { public var collisionWorld:CollisionWorld; @@ -204,6 +207,7 @@ class MarbleWorld extends Scheduler { var maxPredictionTicks:Int = 16; var clientMarbles:Map = []; + var predictions:MarblePredictionStore; public var lastMoves:MarbleUpdateQueue; @@ -246,6 +250,7 @@ class MarbleWorld extends Scheduler { isRecording = false; isWatching = false; lastMoves = new MarbleUpdateQueue(); + predictions = new MarblePredictionStore(); } // Set the network RNG for hunt @@ -768,10 +773,10 @@ class MarbleWorld extends Scheduler { var tmat = Matrix.T(interiorPosition.x, interiorPosition.y, interiorPosition.z); mat.multiply(mat, tmat); - if (hasCollision) - this.collisionWorld.addStaticInterior(interior.collider, mat); + // if (hasCollision) + // this.collisionWorld.addStaticInterior(interior.collider, mat); - interior.isCollideable = false; + interior.isCollideable = hasCollision; interior.setTransform(mat); onFinish(); }); @@ -890,7 +895,7 @@ class MarbleWorld extends Scheduler { public function addInterior(obj:InteriorObject, onFinish:Void->Void) { this.interiors.push(obj); obj.init(cast this, () -> { - // this.collisionWorld.addEntity(obj.collider); + this.collisionWorld.addEntity(obj.collider); if (obj.useInstancing) this.instanceManager.addObject(obj); else @@ -1033,9 +1038,12 @@ class MarbleWorld extends Scheduler { } public function applyReceivedMoves() { + var needsPrediction = 0; if (!lastMoves.ourMoveApplied) { var ourMove = lastMoves.myMarbleUpdate; if (ourMove != null) { + var ourMoveStruct = Net.clientConnection.moveManager.acknowledgeMove(ourMove.move.id, timeState); + lastMoves.ourMoveApplied = true; for (client => arr in lastMoves.otherMarbleUpdates) { var lastMove = null; while (arr.length > 0) { @@ -1047,27 +1055,61 @@ class MarbleWorld extends Scheduler { } } if (lastMove != null) { - if (ourMove.serverTicks == lastMove.serverTicks) + // if (ourMove.serverTicks == lastMove.serverTicks) { + if (ourMoveStruct != null) { + var otherPred = predictions.retrieveState(clientMarbles[Net.clientIdMap[client]], ourMoveStruct.timeState.ticks); + if (otherPred != null) { + if (otherPred.getError(lastMove) > 0.001) { + // trace('Prediction error: ${otherPred.getError(lastMove)}'); + trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + needsPrediction |= 1 << client; + arr.insert(0, lastMove); + } + } else { + trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + needsPrediction |= 1 << client; + arr.insert(0, lastMove); + } + } else { + trace('Desync in General'); clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); - arr.insert(0, lastMove); + needsPrediction |= 1 << client; + arr.insert(0, lastMove); + } + // } } } - marble.unpackUpdate(ourMove); + if (ourMoveStruct != null) { + var ourPred = predictions.retrieveState(marble, ourMoveStruct.timeState.ticks); + if (ourPred != null) { + if (ourPred.getError(ourMove) > 0.001) { + trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + } + } else { + trace('Desync for tick ${ourMoveStruct.timeState.ticks}'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + } + } else { + trace('Desync in General'); + marble.unpackUpdate(ourMove); + needsPrediction |= 1 << Net.clientId; + } } } + return needsPrediction; } - public function applyClientPrediction() { + public function applyClientPrediction(marbleNeedsPrediction:Int) { // First acknowledge the marble's last move so we can get that over with var ourLastMove = lastMoves.myMarbleUpdate; - if (ourLastMove == null) - return; + if (ourLastMove == null || marbleNeedsPrediction == 0) + return -1; var ackLag = @:privateAccess Net.clientConnection.moveManager.queuedMoves.length; - var mvStored = null; - if (!lastMoves.ourMoveApplied) - mvStored = Net.clientConnection.moveManager.acknowledgeMove(ourLastMove.move.id, timeState); - else - return; var ourLastMoveTime = ourLastMove.serverTicks; @@ -1076,97 +1118,87 @@ class MarbleWorld extends Scheduler { var qm = ourQueuedMoves[0]; var advanceTimeState = qm != null ? qm.timeState.clone() : timeState.clone(); advanceTimeState.dt = 0.032; + advanceTimeState.ticks = ourLastMoveTime; - if (qm != null) { - var mvs = qm.powerupStates.copy(); - for (pw in marble.level.powerUps) { - pw.lastPickUpTime = mvs.shift(); + if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { + if (qm != null) { + var mvs = qm.powerupStates.copy(); + for (pw in marble.level.powerUps) { + pw.lastPickUpTime = mvs.shift(); + } + @:privateAccess marble.helicopterEnableTime = qm.helicopterState; + @:privateAccess marble.megaMarbleEnableTime = qm.megaState; + marble.blastAmount = qm.blastAmt; } - @:privateAccess marble.helicopterEnableTime = qm.helicopterState; - @:privateAccess marble.megaMarbleEnableTime = qm.megaState; - marble.blastAmount = qm.blastAmt; } ackLag = ourQueuedMoves.length; // Tick the remaining moves (ours) - if (!lastMoves.ourMoveApplied) { - @:privateAccess this.marble.isNetUpdate = true; - var totalTicksToDo = ourQueuedMoves.length; - var endTick = ourLastMoveTime + totalTicksToDo; - var currentTick = ourLastMoveTime; - //- Std.int(ourLastMove.moveQueueSize - @:privateAccess Net.clientConnection.moveManager.ackRTT); // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT)) - offset; + @:privateAccess this.marble.isNetUpdate = true; + var totalTicksToDo = ourQueuedMoves.length; + var endTick = ourLastMoveTime + totalTicksToDo; + var currentTick = ourLastMoveTime; + //- Std.int(ourLastMove.moveQueueSize - @:privateAccess Net.clientConnection.moveManager.ackRTT); // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT)) - offset; - var marblesToTick = new Map(); + var marblesToTick = new Map(); - for (client => arr in lastMoves.otherMarbleUpdates) { - if (arr.length > 0) { - var m = arr[0]; - if (m.serverTicks == ourLastMoveTime) { - var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; - Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); - m.calculationTicks = ourQueuedMoves.length; // ourQueuedMoves.length; + for (client => arr in lastMoves.otherMarbleUpdates) { + if (marbleNeedsPrediction & (1 << client) > 0 && arr.length > 0) { + var m = arr[0]; + // if (m.serverTicks == ourLastMoveTime) { + var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + if (marbleNeedsPrediction & (1 << Net.clientId) > 0) + m.calculationTicks = ourQueuedMoves.length; // ourQueuedMoves.length; + else + m.calculationTicks = ourQueuedMoves.length; + // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT - ourLastMove.moveQueueSize) / 2); - // - Std.int((@:privateAccess Net.clientConnection.moveManager.ackRTT - ourLastMove.moveQueueSize) / 2); - marblesToTick.set(client, m); - arr.shift(); - } - } + marblesToTick.set(client, m); + arr.shift(); + // } } + } - Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); - // var syncTickStates = new Map(); + Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + // var syncTickStates = new Map(); - for (move in ourQueuedMoves) { - var m = move.move; - // Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + for (move in ourQueuedMoves) { + var m = move.move; + // Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { this.marble.heldPowerup = move.powerup; @:privateAccess this.marble.moveMotionDir = move.motionDir; @:privateAccess this.marble.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); - // var collidings = @:privateAccess this.marble.contactEntities.filter(x -> x is SphereCollisionEntity); - advanceTimeState.currentAttemptTime += 0.032; - - for (client => m in marblesToTick) { - if (m.calculationTicks > 0) { - var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; - // Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); - - var beforePos = @:privateAccess marbleToUpdate.newPos.clone(); - var mv = m.move.move; - @:privateAccess marbleToUpdate.isNetUpdate = true; - @:privateAccess marbleToUpdate.moveMotionDir = m.move.motionDir; - @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, mv, this.collisionWorld, this.pathedInteriors); - @:privateAccess marbleToUpdate.isNetUpdate = false; - m.calculationTicks--; - - var afterPos = @:privateAccess marbleToUpdate.newPos; - } - } - currentTick++; + this.predictions.storeState(this.marble, move.timeState.ticks); } + // var collidings = @:privateAccess this.marble.contactEntities.filter(x -> x is SphereCollisionEntity); - // for (marble => state in syncTickStates) { - // marble.restoreState(state); - // } + for (client => m in marblesToTick) { + if (m.calculationTicks > 0) { + var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + // Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); - // for (client => m in marblesToTick) { - // if (m.calculationTicks >= 0) { - // var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; - - // while (m.calculationTicks > 0) { - // var mv = m.move.move; - // @:privateAccess marbleToUpdate.isNetUpdate = true; - // @:privateAccess marbleToUpdate.moveMotionDir = m.move.motionDir; - // @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, mv, this.collisionWorld, this.pathedInteriors); - // @:privateAccess marbleToUpdate.isNetUpdate = false; - // m.calculationTicks--; - // } - // } - // } - - lastMoves.ourMoveApplied = true; - @:privateAccess this.marble.isNetUpdate = false; + var mv = m.move.move; + @:privateAccess marbleToUpdate.isNetUpdate = true; + @:privateAccess marbleToUpdate.moveMotionDir = m.move.motionDir; + @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, mv, this.collisionWorld, this.pathedInteriors); + this.predictions.storeState(marbleToUpdate, move.timeState.ticks); + @:privateAccess marbleToUpdate.isNetUpdate = false; + m.calculationTicks--; + } + } + advanceTimeState.currentAttemptTime += 0.032; + advanceTimeState.ticks++; + currentTick++; } + + lastMoves.ourMoveApplied = true; + @:privateAccess this.marble.isNetUpdate = false; + return advanceTimeState.ticks; + + return -1; } public function rollback(t:Float) { @@ -1355,10 +1387,11 @@ class MarbleWorld extends Scheduler { tickAccumulator += timeState.dt; while (tickAccumulator >= 0.032) { // Apply the server side ticks + var lastPredTick = -1; if (Net.isClient) { - applyReceivedMoves(); + var marbleNeedsTicking = applyReceivedMoves(); // Catch up - applyClientPrediction(); + lastPredTick = applyClientPrediction(marbleNeedsTicking); } // Do the clientside prediction sim @@ -1371,6 +1404,10 @@ class MarbleWorld extends Scheduler { for (client => marble in clientMarbles) { otherMoves.push(marble.updateServer(fixedDt, collisionWorld, pathedInteriors)); } + this.predictions.storeState(marble, myMove.timeState.ticks); + for (client => marble in clientMarbles) { + this.predictions.storeState(marble, myMove.timeState.ticks); + } if (Net.isHost) { for (client => othermarble in clientMarbles) { // Oh no! var mv = otherMoves.shift(); diff --git a/src/collision/CollisionWorld.hx b/src/collision/CollisionWorld.hx index be4b1f44..ebf59867 100644 --- a/src/collision/CollisionWorld.hx +++ b/src/collision/CollisionWorld.hx @@ -53,7 +53,7 @@ class CollisionWorld { } } - contacts = contacts.concat(this.staticWorld.sphereIntersection(spherecollision, timeState)); + // contacts = contacts.concat(this.staticWorld.sphereIntersection(spherecollision, timeState)); var dynSearch = dynamicOctree.boundingSearch(box).map(x -> cast(x, CollisionEntity)); for (obj in dynSearch) { diff --git a/src/net/MarblePredictionStore.hx b/src/net/MarblePredictionStore.hx new file mode 100644 index 00000000..dedc2e2d --- /dev/null +++ b/src/net/MarblePredictionStore.hx @@ -0,0 +1,62 @@ +package net; + +import net.NetPacket.MarbleUpdatePacket; +import net.NetPacket.MarbleMovePacket; +import src.TimeState; +import src.Marble; +import h3d.Vector; + +@:publicFields +class MarblePrediction { + var tick:Int; + var position:Vector; + var velocity:Vector; + var omega:Vector; + + public function new(marble:Marble, tick:Int) { + this.tick = tick; + position = @:privateAccess marble.newPos.clone(); + velocity = @:privateAccess marble.velocity.clone(); + omega = @:privateAccess marble.omega.clone(); + } + + public inline function getError(p:MarbleUpdatePacket) { + var subs = position.sub(p.position).lengthSq() + velocity.sub(p.velocity).lengthSq() + omega.sub(p.omega).lengthSq(); + return subs; + } +} + +class MarblePredictionStore { + var predictions:Map>; + + public function new() { + predictions = []; + } + + public function storeState(marble:Marble, tick:Int) { + var state = new MarblePrediction(marble, tick); + if (predictions.exists(marble)) { + var arr = predictions[marble]; + while (arr.length != 0 && arr[0].tick >= tick) + arr.shift(); + arr.push(state); + } else { + predictions.set(marble, [state]); + } + } + + public function retrieveState(marble:Marble, tick:Int) { + if (predictions.exists(marble)) { + var arr = predictions[marble]; + while (arr.length != 0 && arr[0].tick < tick) + arr.shift(); + if (arr.length == 0) + return null; + var p = arr[0]; + if (p.tick == tick) + return p; + return null; + } + return null; + } +} diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx index c8124573..26355bf0 100644 --- a/src/net/MoveManager.hx +++ b/src/net/MoveManager.hx @@ -1,5 +1,6 @@ package net; +import net.NetPacket.MarbleUpdatePacket; import shapes.PowerUp; import net.NetPacket.MarbleMovePacket; import src.TimeState; @@ -199,4 +200,20 @@ class MoveManager { lastAckMoveId = m; return mv; } + + public function getMoveForTick(m:Int) { + if (m <= lastAckMoveId) + return null; + if (m == 65535 || m == -1) + return null; + if (queuedMoves.length == 0) + return null; + for (i in 0...queuedMoves.length) { + if (queuedMoves[i].id == m) + return queuedMoves[i]; + if (queuedMoves[i].id > m) + return null; + } + return null; + } } diff --git a/src/net/Net.hx b/src/net/Net.hx index e688b200..a88c8d90 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -50,7 +50,7 @@ class Net { isHost = true; isClient = false; clientId = 0; - masterWs = new WebSocket("ws://localhost:8080"); + masterWs = new WebSocket("ws://192.168.1.2:8080"); masterWs.onmessage = (m) -> { switch (m) { @@ -97,7 +97,7 @@ class Net { } public static function joinServer(connectedCb:() -> Void) { - masterWs = new WebSocket("ws://localhost:8080"); + masterWs = new WebSocket("ws://192.168.1.2:8080"); masterWs.onopen = () -> { client = new RTCPeerConnection(["stun:stun.l.google.com:19302"], "0.0.0.0"); var candidates = [];