mirror of
https://github.com/RandomityGuy/MBHaxe.git
synced 2026-04-27 21:21:41 +00:00
use unreliable datachannels and custom netcode to handle throttles and dropped moves
This commit is contained in:
parent
4adab66389
commit
0c7c2029ba
5 changed files with 170 additions and 32 deletions
|
|
@ -104,6 +104,15 @@ class ProfilerUI {
|
||||||
+ 'Last Ack Move: ${Net.isClient ? @:privateAccess Net.clientConnection.moveManager.lastAckMoveId : 0}\n'
|
+ 'Last Ack Move: ${Net.isClient ? @:privateAccess Net.clientConnection.moveManager.lastAckMoveId : 0}\n'
|
||||||
+ 'Move Ack RTT: ${Net.isClient ? @:privateAccess Net.clientConnection.moveManager.ackRTT : 0}';
|
+ 'Move Ack RTT: ${Net.isClient ? @:privateAccess Net.clientConnection.moveManager.ackRTT : 0}';
|
||||||
}
|
}
|
||||||
|
if (Net.isHost) {
|
||||||
|
var strs = [];
|
||||||
|
strs.push('World Ticks: ${MarbleGame.instance.world.timeState.ticks}');
|
||||||
|
for (dc => cc in Net.clients) {
|
||||||
|
strs.push('${cc.id} move: sz ${@:privateAccess cc.moveManager.getQueueSize()} avg ${@:privateAccess cc.moveManager.serverAvgMoveListSize}');
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.networkStats.text = strs.join('\n');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
instance.networkStats.text = "";
|
instance.networkStats.text = "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,18 @@ enum abstract NetPlatform(Int) from Int to Int {
|
||||||
class ClientConnection extends GameConnection {
|
class ClientConnection extends GameConnection {
|
||||||
var socket:RTCPeerConnection;
|
var socket:RTCPeerConnection;
|
||||||
var datachannel:RTCDataChannel;
|
var datachannel:RTCDataChannel;
|
||||||
|
var datachannelUnreliable:RTCDataChannel;
|
||||||
var rtt:Float;
|
var rtt:Float;
|
||||||
var pingSendTime:Float;
|
var pingSendTime:Float;
|
||||||
var _rttRecords:Array<Float> = [];
|
var _rttRecords:Array<Float> = [];
|
||||||
var lastRecvTime:Float;
|
var lastRecvTime:Float;
|
||||||
var didWarnTimeout:Bool = false;
|
var didWarnTimeout:Bool = false;
|
||||||
|
|
||||||
public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel) {
|
public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel, datachannelUnreliable:RTCDataChannel) {
|
||||||
super(id);
|
super(id);
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.datachannel = datachannel;
|
this.datachannel = datachannel;
|
||||||
|
this.datachannelUnreliable = datachannelUnreliable;
|
||||||
this.state = GameplayState.LOBBY;
|
this.state = GameplayState.LOBBY;
|
||||||
this.rtt = 0;
|
this.rtt = 0;
|
||||||
this.name = "Unknown";
|
this.name = "Unknown";
|
||||||
|
|
@ -43,6 +45,10 @@ class ClientConnection extends GameConnection {
|
||||||
datachannel.sendBytes(b);
|
datachannel.sendBytes(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override function sendBytesUnreliable(b:Bytes) {
|
||||||
|
datachannelUnreliable.sendBytes(b);
|
||||||
|
}
|
||||||
|
|
||||||
public inline function needsTimeoutWarn(t:Float) {
|
public inline function needsTimeoutWarn(t:Float) {
|
||||||
return (t - lastRecvTime) > 10 && !didWarnTimeout;
|
return (t - lastRecvTime) > 10 && !didWarnTimeout;
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +117,8 @@ abstract class GameConnection {
|
||||||
|
|
||||||
public function sendBytes(b:haxe.io.Bytes) {}
|
public function sendBytes(b:haxe.io.Bytes) {}
|
||||||
|
|
||||||
|
public function sendBytesUnreliable(b:haxe.io.Bytes) {}
|
||||||
|
|
||||||
public inline function getName() {
|
public inline function getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,17 @@ class MoveManager {
|
||||||
var ackRTT:Int = -1;
|
var ackRTT:Int = -1;
|
||||||
|
|
||||||
var maxMoves = 45;
|
var maxMoves = 45;
|
||||||
|
var maxSendMoveListSize = 30;
|
||||||
|
|
||||||
var serverTargetMoveListSize = 3;
|
var serverTargetMoveListSize = 4;
|
||||||
var serverMaxMoveListSize = 8;
|
var serverMaxMoveListSize = 8;
|
||||||
var serverAvgMoveListSize = 3.0;
|
var serverAvgMoveListSize = 4.0;
|
||||||
var serverSmoothMoveAvg = 0.15;
|
var serverSmoothMoveAvg = 0.15;
|
||||||
var serverMoveListSizeSlack = 1.0;
|
var serverMoveListSizeSlack = 1.5;
|
||||||
var serverDefaultMinTargetMoveListSize = 3;
|
var serverDefaultMinTargetMoveListSize = 4;
|
||||||
var serverAbnormalMoveCount = 0;
|
var serverAbnormalMoveCount = 0;
|
||||||
|
var serverLastRecvMove = 0;
|
||||||
|
var serverLastAckMove = 0;
|
||||||
|
|
||||||
public var stall = false;
|
public var stall = false;
|
||||||
|
|
||||||
|
|
@ -119,15 +122,19 @@ class MoveManager {
|
||||||
if (nextMoveId >= 65535) // 65535 is reserved for null move
|
if (nextMoveId >= 65535) // 65535 is reserved for null move
|
||||||
nextMoveId = 0;
|
nextMoveId = 0;
|
||||||
|
|
||||||
|
var moveStartIdx = queuedMoves.length - maxSendMoveListSize;
|
||||||
|
if (moveStartIdx < 0)
|
||||||
|
moveStartIdx = 0;
|
||||||
|
|
||||||
var b = new OutputBitStream();
|
var b = new OutputBitStream();
|
||||||
var movePacket = new MarbleMovePacket();
|
var movePacket = new MarbleMovePacket();
|
||||||
movePacket.clientId = Net.clientId;
|
movePacket.clientId = Net.clientId;
|
||||||
movePacket.move = netMove;
|
movePacket.moves = queuedMoves.slice(moveStartIdx);
|
||||||
movePacket.clientTicks = timeState.ticks;
|
movePacket.clientTicks = timeState.ticks;
|
||||||
b.writeByte(NetPacketType.MarbleMove);
|
b.writeByte(NetPacketType.MarbleMove);
|
||||||
movePacket.serialize(b);
|
movePacket.serialize(b);
|
||||||
|
|
||||||
Net.sendPacketToHost(b);
|
Net.sendPacketToHostUnreliable(b);
|
||||||
|
|
||||||
return netMove;
|
return netMove;
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +175,17 @@ class MoveManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public inline function queueMove(m:NetMove) {
|
public inline function queueMove(m:NetMove) {
|
||||||
queuedMoves.push(m);
|
if (serverLastRecvMove < m.id && serverLastAckMove < m.id) {
|
||||||
|
queuedMoves.push(m);
|
||||||
|
serverLastRecvMove = m.id;
|
||||||
|
}
|
||||||
|
// if (queuedMoves.length != 0) {
|
||||||
|
// var lastQueuedMove = queuedMoves[queuedMoves.length - 1];
|
||||||
|
// if (lastQueuedMove.id < m.id)
|
||||||
|
// queuedMoves.push(m);
|
||||||
|
// } else if (lastMove == null || lastMove.id < m.id) {
|
||||||
|
// queuedMoves.push(m);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNextMove() {
|
public function getNextMove() {
|
||||||
|
|
@ -214,6 +231,7 @@ class MoveManager {
|
||||||
} else {
|
} else {
|
||||||
lastMove = queuedMoves[0];
|
lastMove = queuedMoves[0];
|
||||||
queuedMoves.shift();
|
queuedMoves.shift();
|
||||||
|
lastAckMoveId = lastMove.id;
|
||||||
return lastMove;
|
return lastMove;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
src/net/Net.hx
135
src/net/Net.hx
|
|
@ -62,6 +62,7 @@ class ServerInfo {
|
||||||
class Net {
|
class Net {
|
||||||
static var client:RTCPeerConnection;
|
static var client:RTCPeerConnection;
|
||||||
static var clientDatachannel:RTCDataChannel;
|
static var clientDatachannel:RTCDataChannel;
|
||||||
|
static var clientDatachannelUnreliable:RTCDataChannel;
|
||||||
|
|
||||||
public static var isMP:Bool;
|
public static var isMP:Bool;
|
||||||
public static var isHost:Bool;
|
public static var isHost:Bool;
|
||||||
|
|
@ -113,8 +114,22 @@ class Net {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var reliable:datachannel.RTCDataChannel = null;
|
||||||
|
var unreliable:datachannel.RTCDataChannel = null;
|
||||||
peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> {
|
peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> {
|
||||||
onClientConnect(peer, dc);
|
if (dc.name == "mp")
|
||||||
|
reliable = dc;
|
||||||
|
if (dc.name == "unreliable") {
|
||||||
|
unreliable = dc;
|
||||||
|
switch (dc.reliability) {
|
||||||
|
case Reliable:
|
||||||
|
Console.log("Error opening unreliable datachannel!");
|
||||||
|
case Unreliable(maxRetransmits, maxLifetime):
|
||||||
|
Console.log("Opened unreliable datachannel: " + maxRetransmits + " " + maxLifetime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reliable != null && unreliable != null)
|
||||||
|
onClientConnect(peer, reliable, unreliable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,22 +161,34 @@ class Net {
|
||||||
}
|
}
|
||||||
|
|
||||||
clientDatachannel = client.createDatachannel("mp");
|
clientDatachannel = client.createDatachannel("mp");
|
||||||
clientDatachannel.onOpen = (n) -> {
|
clientDatachannelUnreliable = client.createDatachannelWithOptions("unreliable", false, 0, 600);
|
||||||
var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content;
|
|
||||||
if (loadGui != null) {
|
var closing = false;
|
||||||
loadGui.setLoadingStatus("Handshaking");
|
var openFlags = 0;
|
||||||
|
|
||||||
|
var onDatachannelOpen = (idx:Int) -> {
|
||||||
|
openFlags |= idx;
|
||||||
|
if (openFlags == 3) {
|
||||||
|
var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content;
|
||||||
|
if (loadGui != null) {
|
||||||
|
loadGui.setLoadingStatus("Handshaking");
|
||||||
|
}
|
||||||
|
Console.log("Successfully connected!");
|
||||||
|
clients.set(client, new ClientConnection(0, client, clientDatachannel, clientDatachannelUnreliable)); // host is always 0
|
||||||
|
clientIdMap[0] = clients[client];
|
||||||
|
clientConnection = cast clients[client];
|
||||||
|
onConnectedToServer();
|
||||||
|
haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation
|
||||||
}
|
}
|
||||||
Console.log("Successfully connected!");
|
|
||||||
clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0
|
|
||||||
clientIdMap[0] = clients[client];
|
|
||||||
clientConnection = cast clients[client];
|
|
||||||
onConnectedToServer();
|
|
||||||
haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation
|
|
||||||
}
|
}
|
||||||
clientDatachannel.onMessage = (b) -> {
|
var onDatachannelMessage = (dc:RTCDataChannel, b:haxe.io.Bytes) -> {
|
||||||
onPacketReceived(clientConnection, client, clientDatachannel, new InputBitStream(b));
|
onPacketReceived(clientConnection, client, clientDatachannel, new InputBitStream(b));
|
||||||
}
|
}
|
||||||
clientDatachannel.onClosed = () -> {
|
|
||||||
|
var onDatachannelClose = (dc:RTCDataChannel) -> {
|
||||||
|
if (closing)
|
||||||
|
return;
|
||||||
|
closing = true;
|
||||||
var weLeftOurselves = !Net.isClient; // If we left ourselves, this would be set to false due to order of ops, disconnect being called first, and then the datachannel closing
|
var weLeftOurselves = !Net.isClient; // If we left ourselves, this would be set to false due to order of ops, disconnect being called first, and then the datachannel closing
|
||||||
disconnect();
|
disconnect();
|
||||||
if (MarbleGame.instance.world != null) {
|
if (MarbleGame.instance.world != null) {
|
||||||
|
|
@ -175,7 +202,11 @@ class Net {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientDatachannel.onError = (msg) -> {
|
|
||||||
|
var onDatachannelError = (msg:String) -> {
|
||||||
|
if (closing)
|
||||||
|
return;
|
||||||
|
closing = true;
|
||||||
Console.log('Errored out due to ${msg}');
|
Console.log('Errored out due to ${msg}');
|
||||||
disconnect();
|
disconnect();
|
||||||
if (MarbleGame.instance.world != null) {
|
if (MarbleGame.instance.world != null) {
|
||||||
|
|
@ -186,6 +217,31 @@ class Net {
|
||||||
loadGui.setErrorStatus("Connection error");
|
loadGui.setErrorStatus("Connection error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientDatachannel.onOpen = (n) -> {
|
||||||
|
onDatachannelOpen(1);
|
||||||
|
}
|
||||||
|
clientDatachannel.onMessage = (b) -> {
|
||||||
|
onDatachannelMessage(clientDatachannel, b);
|
||||||
|
}
|
||||||
|
clientDatachannel.onClosed = () -> {
|
||||||
|
onDatachannelClose(clientDatachannel);
|
||||||
|
}
|
||||||
|
clientDatachannel.onError = (msg) -> {
|
||||||
|
onDatachannelError(msg);
|
||||||
|
}
|
||||||
|
clientDatachannelUnreliable.onOpen = (n) -> {
|
||||||
|
onDatachannelOpen(2);
|
||||||
|
}
|
||||||
|
clientDatachannelUnreliable.onMessage = (b) -> {
|
||||||
|
onDatachannelMessage(clientDatachannelUnreliable, b);
|
||||||
|
}
|
||||||
|
clientDatachannelUnreliable.onClosed = () -> {
|
||||||
|
onDatachannelClose(clientDatachannelUnreliable);
|
||||||
|
}
|
||||||
|
clientDatachannelUnreliable.onError = (msg) -> {
|
||||||
|
onDatachannelError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
isMP = true;
|
isMP = true;
|
||||||
isHost = false;
|
isHost = false;
|
||||||
isClient = true;
|
isClient = true;
|
||||||
|
|
@ -263,23 +319,54 @@ class Net {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel) {
|
static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel, dcu:RTCDataChannel) {
|
||||||
clientId += 1;
|
clientId += 1;
|
||||||
var cc = new ClientConnection(clientId, c, dc);
|
var cc = new ClientConnection(clientId, c, dc, dcu);
|
||||||
clients.set(c, cc);
|
clients.set(c, cc);
|
||||||
clientIdMap[clientId] = clients[c];
|
clientIdMap[clientId] = clients[c];
|
||||||
dc.onMessage = (msgBytes) -> {
|
|
||||||
|
var closing = false;
|
||||||
|
|
||||||
|
var onMessage = (dc:RTCDataChannel, msgBytes:haxe.io.Bytes) -> {
|
||||||
onPacketReceived(cc, c, dc, new InputBitStream(msgBytes));
|
onPacketReceived(cc, c, dc, new InputBitStream(msgBytes));
|
||||||
}
|
}
|
||||||
dc.onClosed = () -> {
|
var onClosed = () -> {
|
||||||
|
if (closing)
|
||||||
|
return;
|
||||||
|
closing = true;
|
||||||
clients.remove(c);
|
clients.remove(c);
|
||||||
onClientLeave(cc);
|
onClientLeave(cc);
|
||||||
}
|
}
|
||||||
dc.onError = (msg) -> {
|
|
||||||
|
var onError = (msg:String) -> {
|
||||||
|
if (closing)
|
||||||
|
return;
|
||||||
|
closing = true;
|
||||||
clients.remove(c);
|
clients.remove(c);
|
||||||
Console.log('Client ${cc.id} errored out due to: ${msg}');
|
Console.log('Client ${cc.id} errored out due to: ${msg}');
|
||||||
onClientLeave(cc);
|
onClientLeave(cc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dc.onMessage = (msgBytes) -> {
|
||||||
|
onMessage(dc, msgBytes);
|
||||||
|
}
|
||||||
|
dc.onClosed = () -> {
|
||||||
|
onClosed();
|
||||||
|
}
|
||||||
|
dc.onError = (msg) -> {
|
||||||
|
onError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
dcu.onMessage = (msgBytes) -> {
|
||||||
|
onMessage(dcu, msgBytes);
|
||||||
|
}
|
||||||
|
dcu.onClosed = () -> {
|
||||||
|
onClosed();
|
||||||
|
}
|
||||||
|
dcu.onError = (msg) -> {
|
||||||
|
onError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
var b = haxe.io.Bytes.alloc(2);
|
var b = haxe.io.Bytes.alloc(2);
|
||||||
b.set(0, ClientIdAssign);
|
b.set(0, ClientIdAssign);
|
||||||
b.set(1, clientId);
|
b.set(1, clientId);
|
||||||
|
|
@ -444,7 +531,8 @@ class Net {
|
||||||
movePacket.deserialize(input);
|
movePacket.deserialize(input);
|
||||||
var cc = clientIdMap[movePacket.clientId];
|
var cc = clientIdMap[movePacket.clientId];
|
||||||
if (cc.state == GAME)
|
if (cc.state == GAME)
|
||||||
cc.queueMove(movePacket.move);
|
for (move in movePacket.moves)
|
||||||
|
cc.queueMove(move);
|
||||||
|
|
||||||
case PowerupPickup:
|
case PowerupPickup:
|
||||||
var powerupPickupPacket = new PowerupPickupPacket();
|
var powerupPickupPacket = new PowerupPickupPacket();
|
||||||
|
|
@ -532,6 +620,13 @@ class Net {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function sendPacketToHostUnreliable(packetData:OutputBitStream) {
|
||||||
|
if (clientDatachannelUnreliable.state == Open) {
|
||||||
|
var bytes = packetData.getBytes();
|
||||||
|
clientDatachannelUnreliable.sendBytes(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function sendPacketToClient(client:GameConnection, packetData:OutputBitStream) {
|
public static function sendPacketToClient(client:GameConnection, packetData:OutputBitStream) {
|
||||||
var bytes = packetData.getBytes();
|
var bytes = packetData.getBytes();
|
||||||
client.sendBytes(bytes);
|
client.sendBytes(bytes);
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,28 @@ interface NetPacket {
|
||||||
class MarbleMovePacket implements NetPacket {
|
class MarbleMovePacket implements NetPacket {
|
||||||
var clientId:Int;
|
var clientId:Int;
|
||||||
var clientTicks:Int;
|
var clientTicks:Int;
|
||||||
var move:NetMove;
|
var moves:Array<NetMove>;
|
||||||
|
|
||||||
public function new() {}
|
public function new() {
|
||||||
|
moves = [];
|
||||||
|
}
|
||||||
|
|
||||||
public inline function deserialize(b:InputBitStream) {
|
public inline function deserialize(b:InputBitStream) {
|
||||||
clientId = b.readByte();
|
clientId = b.readByte();
|
||||||
clientTicks = b.readUInt16();
|
clientTicks = b.readUInt16();
|
||||||
move = MoveManager.unpackMove(b);
|
var count = b.readInt(5);
|
||||||
|
moves = [];
|
||||||
|
for (i in 0...count) {
|
||||||
|
moves.push(MoveManager.unpackMove(b));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public inline function serialize(b:OutputBitStream) {
|
public inline function serialize(b:OutputBitStream) {
|
||||||
b.writeByte(clientId);
|
b.writeByte(clientId);
|
||||||
b.writeUInt16(clientTicks);
|
b.writeUInt16(clientTicks);
|
||||||
MoveManager.packMove(move, b);
|
b.writeInt(moves.length, 5);
|
||||||
|
for (move in moves)
|
||||||
|
MoveManager.packMove(move, b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue