diff --git a/src/Debug.hx b/src/Debug.hx index fff1fc65..c172e3a6 100644 --- a/src/Debug.hx +++ b/src/Debug.hx @@ -53,6 +53,7 @@ class Debug { debugSphere.setScale(sph.radius); debugSphere.emitInstance(); } + _spheres = []; } else { if (debugSphere != null) { debugSphere.remove(); diff --git a/src/Marble.hx b/src/Marble.hx index 62125a3f..ee9eb2fc 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -702,6 +702,8 @@ class Marble extends GameObject { normP.scale(1 + bounce); + velocity.load(velocity.sub(normP.multiply(ourMass))); + otherMarble.velocity.load(otherMarble.velocity.add(normP.multiply(1 / theirMass))); contacts[i].velocity.load(otherMarble.velocity); } else { @@ -1048,7 +1050,8 @@ class Marble extends GameObject { found: false, foundContacts: [], lastContactPos: null, - lastContactNormal: null + lastContactNormal: null, + foundMarbles: [], }; } var searchbox = new Bounds(); @@ -1065,6 +1068,34 @@ class Marble extends GameObject { var testTriangles = []; var finalContacts = []; + var foundMarbles = []; + + // Marble-Marble + var nextPos = position.add(velocity.multiply(deltaT)); + for (marble in this.collisionWorld.marbleEntities) { + if (marble == this.collider) + continue; + var otherPosition = marble.transform.getPosition(); + var isec = Collision.capsuleSphereNearestOverlap(position, nextPos, _radius, otherPosition, marble.radius); + if (isec.result) { + foundMarbles.push(marble); + isec.t *= deltaT; + if (isec.t >= finalT) { + var vel = position.add(velocity.multiply(finalT)).sub(otherPosition); + vel.normalize(); + var newVelLen = this.velocity.sub(marble.velocity).dot(vel); + if (newVelLen < 0.0) { + finalT = isec.t; + + var posDiff = nextPos.sub(position).multiply(isec.t); + var p = posDiff.add(position); + lastContactNormal = p.sub(otherPosition); + lastContactNormal.normalize(); + lastContactPos = p.sub(lastContactNormal.multiply(_radius)); + } + } + } + } // for (iter in 0...10) { // var iterationFound = false; @@ -1379,13 +1410,14 @@ class Marble extends GameObject { foundContacts: testTriangles, lastContactPos: lastContactPos, lastContactNormal: position.sub(lastContactPos).normalized(), + foundMarbles: foundMarbles }; } function nudgeToContacts(position:Vector, radius:Float, foundContacts:Array<{ v:Array, n:Vector - }>) { + }>, foundMarbles:Array) { var it = 0; var concernedContacts = foundContacts; // PathedInteriors have their own nudge logic var prevResolved = 0; @@ -1444,6 +1476,14 @@ class Marble extends GameObject { prevResolved = resolved; it++; } while (true && it < 10); + for (marble in foundMarbles) { + var marblePosition = marble.transform.getPosition(); + 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)); + } + } return position; } @@ -1547,7 +1587,7 @@ class Marble extends GameObject { } var expectedPos = finalPosData.position; // var newPos = expectedPos; - var newPos = nudgeToContacts(expectedPos, _radius, finalPosData.foundContacts); + var newPos = nudgeToContacts(expectedPos, _radius, finalPosData.foundContacts, finalPosData.foundMarbles); if (this.velocity.lengthSq() > 1e-8) { var posDiff = newPos.sub(expectedPos); @@ -1648,11 +1688,19 @@ class Marble extends GameObject { public function unpackUpdate(p:MarbleUpdatePacket) { // Assume packet header is already read + // Check if we aren't colliding with a marble + // for (marble in this.level.collisionWorld.marbleEntities) { + // if (marble != this.collider && marble.transform.getPosition().distance(p.position) < marble.radius + this._radius) { + // Console.log("Marble updated inside another one!"); + // return false; + // } + // } this.oldPos = this.newPos; this.newPos = p.position; this.collider.transform.setPosition(this.newPos); this.velocity = p.velocity; this.omega = p.omega; + return true; } public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array, packets:Array) { diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 427bbec9..9f955e75 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,6 @@ package src; +import haxe.Exception; import net.NetPacket.MarbleUpdatePacket; import net.NetPacket.MarbleMovePacket; import net.MoveManager; @@ -1022,48 +1023,178 @@ class MarbleWorld extends Scheduler { } public function applyReceivedMoves() { - for (client => lastMove in lastMoves) { - if (lastMove.applied) - continue; - if (lastMove.clientId == Net.clientId) - marble.unpackUpdate(lastMove); - else - clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + if (!lastMoves[Net.clientId].applied) { + var allApplied = false; + for (client => lastMove in lastMoves) { + if (lastMove.applied) { + allApplied = true; + break; + } + } + if (!allApplied) { + for (client => lastMove in lastMoves) { + var isApplied = lastMove.clientId == Net.clientId ? marble.unpackUpdate(lastMove) : clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + } + } } + // for (client => lastMove in lastMoves) { + // if (lastMove.applied) + // continue; + + // var isApplied = lastMove.clientId == Net.clientId ? marble.unpackUpdate(lastMove) : clientMarbles[Net.clientIdMap[client]].unpackUpdate(lastMove); + + // if (!isApplied) + // lastMove.applied = true; + // } } public function applyClientPrediction() { - for (client => lastMove in lastMoves) { - if (lastMove.applied) - continue; - var marbleToUpdate = lastMove.clientId == Net.clientId ? marble : clientMarbles[Net.clientIdMap[client]]; + // First acknowledge the marble's last move so we can get that over with + var ourLastMove = lastMoves[Net.clientId]; + if (ourLastMove == null) + return; + var ackLag = -1; + if (!ourLastMove.applied) + ackLag = Net.clientConnection.moveManager.acknowledgeMove(ourLastMove.move.id); + else + return; - @:privateAccess marbleToUpdate.isNetUpdate = true; - if (marbleToUpdate == marble) { - var moveManager = @:privateAccess Net.clientConnection.moveManager; - var catchUpTickCount = 0; - Net.clientConnection.moveManager.acknowledgeMove(lastMove.move.id); - var advanceTimeState = timeState.clone(); - advanceTimeState.dt = 0.032; - for (move in @:privateAccess moveManager.queuedMoves) { - @:privateAccess marbleToUpdate.moveMotionDir = move.motionDir; - @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, move.move, this.collisionWorld, this.pathedInteriors); - } - } else { - var tickDiff = timeState.ticks - lastMove.serverTicks; - if (tickDiff > this.maxPredictionTicks) - tickDiff = this.maxPredictionTicks; - if (tickDiff > 0) { - var advanceTimeState = timeState.clone(); - advanceTimeState.dt = 0.032; - var m = lastMove.move.move; - @:privateAccess marbleToUpdate.moveMotionDir = lastMove.move.motionDir; - for (o in 0...(tickDiff + 1)) { + var ourLastMoveTime = ourLastMove.serverTicks; + + // Then find the minimum tick from which we need to begin our predictions from + var tickStart = timeState.ticks; + var ourQueuedMoves = @:privateAccess Net.clientConnection.moveManager.queuedMoves.copy(); + if (ourQueuedMoves.length > 0 && ourQueuedMoves[0].timeState.ticks < tickStart) + tickStart = ourQueuedMoves[0].timeState.ticks; + + var advanceTimeState = timeState.clone(); + advanceTimeState.dt = 0.032; + /* + for (client => lastMove in lastMoves) { + if (lastMove.applied) + continue; + if (lastMove.serverTicks < tickStart) + tickStart = lastMove.serverTicks; + } + + // Now actually do the sim, tick by tick + for (tick in tickStart...timeState.ticks) { + for (client => lastMove in lastMoves) { + if (lastMove.applied || (tick < lastMove.serverTicks && lastMove.clientId != Net.clientId)) + continue; + + var marbleToUpdate = lastMove.clientId == Net.clientId ? marble : clientMarbles[Net.clientIdMap[client]]; + @:privateAccess marbleToUpdate.isNetUpdate = true; + if (marbleToUpdate == marble) { + if (ourQueuedMoves.length > 0) { + if (ourQueuedMoves[0].timeState.ticks <= tick) { + var move = ourQueuedMoves.shift(); + Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + @:privateAccess marbleToUpdate.moveMotionDir = move.motionDir; + @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, move.move, this.collisionWorld, this.pathedInteriors); + } + } else { + lastMove.applied = true; + } + } else { + var m = lastMove.move.move; + Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + @:privateAccess marbleToUpdate.moveMotionDir = lastMove.move.motionDir; @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); } + @:privateAccess marbleToUpdate.isNetUpdate = false; } } - @:privateAccess marbleToUpdate.isNetUpdate = false; + */ + + // Tick the remaining moves (ours) + if (!ourLastMove.applied) { + @:privateAccess this.marble.isNetUpdate = true; + var totalTicksToDo = ourQueuedMoves.length; + var endTick = ourLastMoveTime + totalTicksToDo; + for (move in ourQueuedMoves) { + var m = move.move; + Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); + @:privateAccess this.marble.moveMotionDir = move.motionDir; + @:privateAccess this.marble.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); + + // for (client => lastMove in lastMoves) { + // if (lastMove.clientId == Net.clientId || lastMove.calculationTicks >= endTick) + // continue; + + // trace('tick diff: ${lastMove.serverTicks - ourLastMoveTime}'); + + // lastMove.calculationTicks++; + // // lastMove.serverTicks++; + + // var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + // @:privateAccess marbleToUpdate.isNetUpdate = true; + // var m = lastMove.move.move; + // @:privateAccess marbleToUpdate.moveMotionDir = lastMove.move.motionDir; + // @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); + // @:privateAccess marbleToUpdate.isNetUpdate = false; + // } + } + + for (client => lastMove in lastMoves) { + if (lastMove.clientId == Net.clientId) + continue; + + // lastMove.calculationTicks++; + // lastMove.serverTicks++; + + var tickDiff = ackLag + 1; // - (lastMove.serverTicks - ourLastMoveTime) + 1; + while (tickDiff > 0) { + var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + @:privateAccess marbleToUpdate.isNetUpdate = true; + var m = lastMove.move.move; + @:privateAccess marbleToUpdate.moveMotionDir = lastMove.move.motionDir; + @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); + @:privateAccess marbleToUpdate.isNetUpdate = false; + tickDiff--; + } + } + + // if (ourQueuedMoves.length >= 2) { + // trace('Move queue tick diff: ${ourQueuedMoves[ourQueuedMoves.length - 1].timeState.ticks - ourQueuedMoves[0].timeState.ticks}'); + // } + ourLastMove.applied = true; + @:privateAccess this.marble.isNetUpdate = false; + } + + // for (client => lastMove in lastMoves) { + // if (lastMove.applied) + // continue; + // if (lastMove.serverTicks > timeState.ticks) { + // trace('Marble ticked ahead ${lastMove.serverTicks - timeState.ticks} ticks'); + // lastMove.serverTicks = timeState.ticks; + // } + // if (lastMove.serverTicks < tickStart) + // tickStart = lastMove.serverTicks; + // } + + // for (tick in tickStart...timeState.ticks) { + // for (client => lastMove in lastMoves) { + // if (lastMove.applied || tick < lastMove.serverTicks) + // continue; + + // // if (lastMove.calculationTicks > 0) { + // // lastMove.calculationTicks--; + // // continue; + // // } + + // var marbleToUpdate = clientMarbles[Net.clientIdMap[client]]; + // @:privateAccess marbleToUpdate.isNetUpdate = true; + // var m = lastMove.move.move; + // Debug.drawSphere(@:privateAccess marbleToUpdate.newPos, marbleToUpdate._radius); + // @:privateAccess marbleToUpdate.moveMotionDir = lastMove.move.motionDir; + // @:privateAccess marbleToUpdate.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); + // @:privateAccess marbleToUpdate.isNetUpdate = false; + // } + // } + + // Now mark them all as applied + for (client => lastMove in lastMoves) { lastMove.applied = true; } } diff --git a/src/collision/Collision.hx b/src/collision/Collision.hx index 5bd2c4df..53ef97ce 100644 --- a/src/collision/Collision.hx +++ b/src/collision/Collision.hx @@ -477,4 +477,51 @@ class Collision { } return null; } + + public static function capsuleSphereNearestOverlap(a0:Vector, a1:Vector, radA:Float, b:Vector, radB:Float) { + var V = a1.sub(a0); + var A0B = a0.sub(b); + var d1 = A0B.dot(V); + var d2 = A0B.dot(A0B); + var d3 = V.dot(V); + var R2 = (radA + radB) * (radA + radB); + if (d2 < R2) { + // starting in collision state + return { + result: true, + t: 0.0 + } + } + if (d3 < 0.01) // no movement, and don't start in collision state, so no collision + return { + result: false, + t: 0.0 + } + + var b24ac = Math.sqrt(d1 * d1 - d2 * d3 + d3 * R2); + var t1 = (-d1 - b24ac) / d3; + if (t1 > 0 && t1 < 1.0) { + return { + result: true, + t: t1 + } + } + var t2 = (-d1 + b24ac) / d3; + if (t2 > 0 && t2 < 1.0) { + return { + result: true, + t: t2 + } + } + if (t1 < 0 && t2 > 0) { + return { + result: true, + t: 0.0 + } + } + return { + result: false, + t: 0.0 + } + } } diff --git a/src/collision/CollisionWorld.hx b/src/collision/CollisionWorld.hx index c4fb0992..4558e479 100644 --- a/src/collision/CollisionWorld.hx +++ b/src/collision/CollisionWorld.hx @@ -13,7 +13,7 @@ class CollisionWorld { public var dynamicEntities:Array = []; public var dynamicOctree:Octree; - var marbleEntities:Array = []; + public var marbleEntities:Array = []; var dynamicEntitySet:Map = []; diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx index 7e475c51..d9702405 100644 --- a/src/net/MoveManager.hx +++ b/src/net/MoveManager.hx @@ -36,7 +36,7 @@ class MoveManager { var lastMove:NetMove; var lastAckMoveId:Int = -1; - static var maxMoves = 16; + static var maxMoves = 32; public function new(connection:ClientConnection) { queuedMoves = []; @@ -155,16 +155,20 @@ class MoveManager { public function acknowledgeMove(m:Int) { if (m == 65535 || m == -1) - return; + return -1; if (m <= lastAckMoveId) - return; // Already acked + return -1; // Already acked if (queuedMoves.length == 0) - return; + return -1; while (m != queuedMoves[0].id) { queuedMoves.shift(); } - if (m == queuedMoves[0].id) + var delta = -1; + if (m == queuedMoves[0].id) { + delta = queuedMoves[0].id - lastAckMoveId; queuedMoves.shift(); + } lastAckMoveId = m; + return delta; } } diff --git a/src/net/NetPacket.hx b/src/net/NetPacket.hx index 0106fd19..c6ab53ba 100644 --- a/src/net/NetPacket.hx +++ b/src/net/NetPacket.hx @@ -34,6 +34,7 @@ class MarbleUpdatePacket implements NetPacket { var clientId:Int; var move:NetMove; var serverTicks:Int; + var calculationTicks:Int; var position:Vector; var velocity:Vector; var omega:Vector; @@ -60,6 +61,7 @@ class MarbleUpdatePacket implements NetPacket { clientId = b.readUInt16(); move = MoveManager.unpackMove(b); serverTicks = b.readUInt16(); + calculationTicks = serverTicks; position = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); velocity = new Vector(b.readFloat(), b.readFloat(), b.readFloat()); omega = new Vector(b.readFloat(), b.readFloat(), b.readFloat());