begin netcode

This commit is contained in:
RandomityGuy 2024-01-24 22:37:05 +05:30
parent 73a7366307
commit 027f65f85b
19 changed files with 1314 additions and 161 deletions

View file

@ -1,6 +1,8 @@
-cp src
-lib heaps
-lib hlsdl
-lib hxWebSockets
-lib datachannel
-hl marblegame.hl
-D windowSize=1280x720
-D keep-inline-positions

38
server/Signalling.hx Normal file
View file

@ -0,0 +1,38 @@
import haxe.Json;
import hx.ws.SocketImpl;
import hx.ws.WebSocketHandler;
import hx.ws.WebSocketServer;
using Lambda;
class SignallingHandler extends WebSocketHandler {
static var clients:Array<SignallingHandler> = [];
public function new(s:SocketImpl) {
super(s);
onopen = () -> {
clients.push(this);
}
onclose = () -> {
clients.remove(this);
}
onmessage = (m) -> {
switch (m) {
case StrMessage(content):
var conts = Json.parse(content);
if (conts.type == "connect") {
var other = clients.find(x -> x != this);
other.send(Json.stringify(conts.sdpObj));
}
case _: {}
}
}
}
}
class Signalling {
static function main() {
var ws = new WebSocketServer<SignallingHandler>("0.0.0.0", 8080, 2);
ws.start();
}
}

View file

@ -0,0 +1,5 @@
--library hxWebSockets
--library datachannel
--main Signalling
-cp .
--hl bin/signalling.hl

View file

@ -184,6 +184,9 @@ class Console {
log('Allocation Count: ${gc.allocationCount}');
log('Memory usage: ${gc.currentMemory}');
#end
} else if (cmdType == 'rollback') {
var t = Std.parseFloat(cmdSplit[1]);
MarbleGame.instance.world.rollback(t);
} else {
error("Unknown command");
}

View file

@ -298,94 +298,4 @@ class DynamicPolygon extends MeshPrimitive {
else
engine.renderMultiBuffers(bufs, engine.mem.triIndexes, 0, triCount());
}
#if hxbit
override function customSerialize(ctx:hxbit.Serializer) {
ctx.addInt(points.length);
for (p in points) {
ctx.addDouble(p.x);
ctx.addDouble(p.y);
ctx.addDouble(p.z);
}
if (normals == null)
ctx.addInt(0);
else {
ctx.addInt(normals.length);
for (p in normals) {
ctx.addDouble(p.x);
ctx.addDouble(p.y);
ctx.addDouble(p.z);
}
}
if (tangents == null)
ctx.addInt(0);
else {
ctx.addInt(tangents.length);
for (p in tangents) {
ctx.addDouble(p.x);
ctx.addDouble(p.y);
ctx.addDouble(p.z);
}
}
if (uvs == null)
ctx.addInt(0);
else {
ctx.addInt(uvs.length);
for (uv in uvs) {
ctx.addDouble(uv.u);
ctx.addDouble(uv.v);
}
}
if (idx == null)
ctx.addInt(0);
else {
ctx.addInt(idx.length);
for (i in idx)
ctx.addInt(i);
}
if (colors == null)
ctx.addInt(0);
else {
ctx.addInt(colors.length);
for (c in colors) {
ctx.addDouble(c.x);
ctx.addDouble(c.y);
ctx.addDouble(c.z);
}
}
}
override function customUnserialize(ctx:hxbit.Serializer) {
points = [
for (i in 0...ctx.getInt())
new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble())
];
normals = [
for (i in 0...ctx.getInt())
new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble())
];
tangents = [
for (i in 0...ctx.getInt())
new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble())
];
uvs = [for (i in 0...ctx.getInt()) new UV(ctx.getDouble(), ctx.getDouble())];
if (normals.length == 0)
normals = null;
if (uvs.length == 0)
uvs = null;
var nindex = ctx.getInt();
if (nindex > 0) {
idx = new hxd.IndexBuffer();
idx.grow(nindex);
for (i in 0...nindex)
idx[i] = ctx.getInt();
}
colors = [
for (i in 0...ctx.getInt())
new h3d.col.Point(ctx.getDouble(), ctx.getDouble(), ctx.getDouble())
];
if (colors.length == 0)
colors = null;
}
#end
}

View file

@ -1,5 +1,6 @@
package;
import datachannel.RTC;
import gui.VersionGui;
import gui.PresentsGui;
import src.Debug;
@ -81,6 +82,7 @@ class Main extends hxd.App {
#end
// try {
RTC.init();
Http.init();
haxe.MainLoop.add(() -> Http.loop());
Settings.init();
@ -143,6 +145,7 @@ class Main extends hxd.App {
// marbleGame.update(1 / 60);
// timeAccumulator -= 1 / 60;
// }
RTC.processEvents();
marbleGame.update(dt);
// } catch (e) {
// Console.error(e.message);

View file

@ -66,6 +66,7 @@ import src.ResourceLoaderWorker;
import src.InteriorObject;
import src.Console;
import src.Gamepad;
import net.Net;
class Move {
public var d:Vector;
@ -284,12 +285,15 @@ class Marble extends GameObject {
public var cubemapRenderer:CubemapRenderer;
var connection:net.Net.ClientConnection;
public function new() {
super();
this.velocity = new Vector();
this.omega = new Vector();
this.camera = new CameraController(cast this);
this.isCollideable = true;
this.bounceEmitterData = new ParticleData();
this.bounceEmitterData.identifier = "MarbleBounceParticle";
@ -318,8 +322,9 @@ class Marble extends GameObject {
this.helicopterSound.pause = true;
}
public function init(level:MarbleWorld, onFinish:Void->Void) {
public function init(level:MarbleWorld, connection:ClientConnection, onFinish:Void->Void) {
this.level = level;
this.connection = connection;
if (this.level != null)
this.collisionWorld = this.level.collisionWorld;
@ -522,6 +527,8 @@ class Marble extends GameObject {
public function getMarbleAxis() {
var motiondir = new Vector(0, -1, 0);
if (level.isReplayingMovement)
return level.currentInputMoves[1].marbleAxes;
if (this.controllable) {
motiondir.transform(Matrix.R(0, 0, camera.CameraYaw));
motiondir.transform(level.newOrientationQuat.toMatrix());
@ -561,7 +568,7 @@ class Marble extends GameObject {
for (contact in contacts) {
if (contact.force != 0 && !forceObjects.contains(contact.otherObject)) {
if (contact.otherObject is RoundBumper) {
if (!playedSounds.contains("data/sound/bumperding1.wav")) {
if (!level.isReplayingMovement && !playedSounds.contains("data/sound/bumperding1.wav")) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/bumperding1.wav", ResourceLoader.getAudio, this.soundResources));
playedSounds.push("data/sound/bumperding1.wav");
}
@ -605,6 +612,8 @@ class Marble extends GameObject {
var R = currentGravityDir.multiply(-this._radius);
var rollVelocity = this.omega.cross(R);
var axes = this.getMarbleAxis();
if (!level.isReplayingMovement)
level.inputRecorder.recordAxis(axes);
var sideDir = axes[0];
var motionDir = axes[1];
var upDir = axes[2];
@ -799,7 +808,7 @@ class Marble extends GameObject {
}
if (sv < this._jumpImpulse) {
this.velocity.load(this.velocity.add(bestContact.normal.multiply((this._jumpImpulse - sv))));
if (!playedSounds.contains("data/sound/jump.wav")) {
if (!level.isReplayingMovement && !playedSounds.contains("data/sound/jump.wav")) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/jump.wav", ResourceLoader.getAudio, this.soundResources));
playedSounds.push("data/sound/jump.wav");
}
@ -879,7 +888,7 @@ class Marble extends GameObject {
}
function bounceEmitter(speed:Float, normal:Vector) {
if (!this.controllable)
if (!this.controllable || level.isReplayingMovement)
return;
if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) {
this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData,
@ -914,6 +923,8 @@ class Marble extends GameObject {
}
function playBoundSound(time:Float, contactVel:Float) {
if (level.isReplayingMovement)
return;
if (minVelocityBounceSoft <= contactVel) {
var hardBounceSpeed = minVelocityBounceHard;
var bounceSoundNum = Math.floor(Math.random() * 4);
@ -943,6 +954,8 @@ class Marble extends GameObject {
}
function updateRollSound(time:TimeState, contactPct:Float, slipAmount:Float) {
if (level.isReplayingMovement)
return;
var rSpat = rollSound.getEffect(Spatialization);
rSpat.position = this.collider.transform.getPosition();
@ -1542,6 +1555,7 @@ class Marble extends GameObject {
// this.setPosition(newPos.x, newPos.y, newPos.z);
this.collider.setTransform(totMatrix);
this.collisionWorld.updateTransform(this.collider);
this.collider.velocity = this.velocity;
if (this.heldPowerup != null && m.powerup && !this.level.outOfBounds) {
@ -1592,41 +1606,139 @@ class Marble extends GameObject {
this.updateRollSound(timeState, contactTime / timeState.dt, this._slipAmount);
}
public function update(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array<PathedInterior>) {
var move = new Move();
move.d = new Vector();
if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching) {
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;
// 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();
}
if (!this.controllable && this.connection != null) {
move = new Move();
move.d = new Vector(0, 0);
}
playedSounds = [];
advancePhysics(timeState, move, collisionWorld, pathedInteriors);
physicsAccumulator = 0;
}
public function updateClient(timeState:TimeState, pathedInteriors:Array<PathedInterior>) {
if (oldPos != null && newPos != null) {
var deltaT = physicsAccumulator / 0.032;
var renderPos = Util.lerpThreeVectors(this.oldPos, this.newPos, deltaT);
this.setPosition(renderPos.x, renderPos.y, renderPos.z);
var rot = this.prevRot;
var quat = new Quat();
quat.initRotation(omega.x * physicsAccumulator, omega.y * physicsAccumulator, omega.z * physicsAccumulator);
quat.multiply(quat, rot);
this.setRotationQuat(quat);
var adt = timeState.clone();
adt.dt = physicsAccumulator;
for (pi in pathedInteriors) {
pi.update(adt);
}
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;
}
physicsAccumulator += timeState.dt;
if (this.controllable && this.level != null && !this.level.rewinding) {
// this.camera.startCenterCamera();
this.camera.update(timeState.currentAttemptTime, timeState.dt);
}
updatePowerupStates(timeState.currentAttemptTime, timeState.dt);
var s = this._renderScale * this._renderScale;
if (s <= this._marbleScale * this._marbleScale)
s = 0.1;
else
s = 0.4;
s = timeState.dt / s * 2.302585124969482;
s = 1.0 / (s * (s * 0.2349999994039536 * s) + s + 1.0 + 0.4799999892711639 * s * s);
this._renderScale *= s;
s = 1 - s;
this._renderScale += s * this._marbleScale;
var marbledts = cast(this.getChildAt(0), DtsObject);
marbledts.setScale(this._renderScale);
if (this._radius != 0.675 && timeState.currentAttemptTime - this.megaMarbleEnableTime < 10) {
this._prevRadius = this._radius;
this._radius = 0.675;
this.collider.radius = 0.675;
this._marbleScale *= 2.25;
var boost = this.level.currentUp.multiply(5);
this.velocity = this.velocity.add(boost);
} else if (timeState.currentAttemptTime - this.megaMarbleEnableTime > 10) {
if (this._radius != this._prevRadius) {
this._radius = this._prevRadius;
this.collider.radius = this._radius;
this._marbleScale = this._defaultScale;
AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null, false);
}
}
this.updateFinishAnimation(timeState.dt);
if (this.mode == Finish) {
this.setPosition(this.finishAnimPosition.x, this.finishAnimPosition.y, this.finishAnimPosition.z);
updatePowerupStates(timeState.currentAttemptTime, timeState.dt);
}
this.trailEmitter();
if (bounceEmitDelay > 0)
bounceEmitDelay -= timeState.dt;
if (bounceEmitDelay < 0)
bounceEmitDelay = 0;
}
public function recordMove() {
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;
}
return move;
}
// SP only function
public function update(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();
}
if (level.isReplayingMovement)
move = level.currentInputMoves[1].move;
if (this.controllable && this.level.isWatching) {
move = new Move();
if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(Jumped))
move.jump = true;
if (this.level.replay.currentPlaybackFrame.marbleStateFlags.has(UsedPowerup))
@ -1638,6 +1750,10 @@ class Marble extends GameObject {
this.level.replay.recordMarbleInput(move.d.x, move.d.y);
}
}
if (!this.controllable && this.connection != null) {
move = new Move();
move.d = new Vector(0, 0);
}
physicsAccumulator += timeState.dt;

View file

@ -318,14 +318,14 @@ class MarbleGame {
Settings.save();
}
public function playMission(mission:Mission) {
public function playMission(mission:Mission, multiplayer:Bool = false) {
canvas.clearContent();
destroyPreviewWorld();
if (world != null) {
world.dispose();
}
Analytics.trackLevelPlay(mission.title, mission.path);
world = new MarbleWorld(scene, scene2d, mission, toRecord);
world = new MarbleWorld(scene, scene2d, mission, toRecord, multiplayer);
world.init();
}

View file

@ -1,5 +1,9 @@
package src;
import net.NetCommands;
import net.Net;
import net.Net.ClientConnection;
import rewind.InputRecorder;
import gui.AchievementsGui;
import src.Radar;
import gui.LevelSelectGui;
@ -106,7 +110,6 @@ class MarbleWorld extends Scheduler {
public var interiors:Array<InteriorObject> = [];
public var pathedInteriors:Array<PathedInterior> = [];
public var marbles:Array<Marble> = [];
public var dtsObjects:Array<DtsObject> = [];
public var forceObjects:Array<ForceObject> = [];
public var triggers:Array<Trigger> = [];
@ -184,6 +187,18 @@ class MarbleWorld extends Scheduler {
public var rewindManager:RewindManager;
public var rewinding:Bool = false;
public var inputRecorder:InputRecorder;
public var isReplayingMovement:Bool = false;
public var currentInputMoves:Array<InputRecorderFrame>;
// Multiplayer
public var isMultiplayer:Bool;
public var startRealTime:Float = 0;
public var multiplayerStarted:Bool = false;
var clientMarbles:Map<ClientConnection, Marble> = [];
// Loading
var resourceLoadFuncs:Array<(() -> Void)->Void> = [];
@ -208,7 +223,7 @@ class MarbleWorld extends Scheduler {
var lock:Bool = false;
public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission, record:Bool = false) {
public function new(scene:Scene, scene2d:h2d.Scene, mission:Mission, record:Bool = false, multiplayer:Bool = false) {
this.scene = scene;
this.scene2d = scene2d;
this.mission = mission;
@ -217,6 +232,17 @@ class MarbleWorld extends Scheduler {
this.replay = new Replay(mission.path, mission.isClaMission ? mission.id : 0);
this.isRecording = record;
this.rewindManager = new RewindManager(this);
this.inputRecorder = new InputRecorder(this);
this.isMultiplayer = multiplayer;
// Set the network RNG for hunt
if (isMultiplayer && gameMode is modes.HuntMode && Net.isHost) {
var hunt:modes.HuntMode = cast gameMode;
var rng = Math.random() * 10000;
NetCommands.setNetworkRNG(rng);
@:privateAccess hunt.rng.setSeed(cast rng);
@:privateAccess hunt.rng2.setSeed(cast rng);
}
}
public function init() {
@ -267,7 +293,12 @@ class MarbleWorld extends Scheduler {
scanMission(this.mission.root);
this.gameMode.missionScan(this.mission);
this.resourceLoadFuncs.push(fwd -> this.initScene(fwd));
this.resourceLoadFuncs.push(fwd -> this.initMarble(fwd));
if (this.isMultiplayer) {
for (client in Net.clients) {
this.resourceLoadFuncs.push(fwd -> this.initMarble(client, fwd)); // Others
}
}
this.resourceLoadFuncs.push(fwd -> this.initMarble(null, fwd)); // Self
this.resourceLoadFuncs.push(fwd -> {
this.addSimGroup(this.mission.root);
this._loadingLength = resourceLoadFuncs.length;
@ -371,7 +402,7 @@ class MarbleWorld extends Scheduler {
worker.run();
}
public function initMarble(onFinish:Void->Void) {
public function initMarble(client:ClientConnection, onFinish:Void->Void) {
Console.log("Initializing marble");
var worker = new ResourceLoaderWorker(onFinish);
var marblefiles = [
@ -422,8 +453,9 @@ class MarbleWorld extends Scheduler {
}
worker.addTask(fwd -> {
var marble = new Marble();
marble.controllable = true;
this.addMarble(marble, fwd);
if (client == null)
marble.controllable = true;
this.addMarble(marble, client, fwd);
});
worker.run();
}
@ -435,6 +467,8 @@ class MarbleWorld extends Scheduler {
interior.onLevelStart();
for (shape in this.dtsObjects)
shape.onLevelStart();
if (this.isMultiplayer && Net.isClient)
NetCommands.clientIsReady(Net.clientId);
}
public function restart(full:Bool = false) {
@ -527,6 +561,15 @@ class MarbleWorld extends Scheduler {
this.marble.setMode(Start);
sky.follow = marble.camera;
if (isMultiplayer) {
for (client => marble in clientMarbles) {
var marbleStartQuat = this.gameMode.getSpawnTransform();
marble.setMarblePosition(marbleStartQuat.position.x, marbleStartQuat.position.y, marbleStartQuat.position.z);
marble.reset();
marble.setMode(Start);
}
}
var missionInfo:MissionElementScriptObject = cast this.mission.root.elements.filter((element) -> element._type == MissionElementType.ScriptObject
&& element._name == "MissionInfo")[0];
if (missionInfo.starthelptext != null)
@ -586,17 +629,32 @@ class MarbleWorld extends Scheduler {
AudioManager.playSound(ResourceLoader.getResource('data/sound/spawn_alternate.wav', ResourceLoader.getAudio, this.soundResources));
}
public function allClientsReady() {
NetCommands.setStartTime(3); // Start after 3 seconds
}
public function updateGameState() {
if (this.outOfBounds)
return; // We will update state manually
if (this.timeState.currentAttemptTime < 0.5) {
this.marble.setMode(Start);
}
if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 3.5)) {
this.marble.setMode(Start);
}
if (this.timeState.currentAttemptTime + skipStartBugPauseTime >= 3.5 && this.finishTime == null) {
this.marble.setMode(Play);
if (!this.isMultiplayer) {
if (this.timeState.currentAttemptTime < 0.5) {
this.marble.setMode(Start);
}
if ((this.timeState.currentAttemptTime >= 0.5) && (this.timeState.currentAttemptTime < 3.5)) {
this.marble.setMode(Start);
}
if (this.timeState.currentAttemptTime + skipStartBugPauseTime >= 3.5 && this.finishTime == null) {
this.marble.setMode(Play);
}
} else {
if (!this.multiplayerStarted) {
if (this.startRealTime != 0 && this.timeState.timeSinceLoad > this.startRealTime) {
this.multiplayerStarted = true;
this.marble.setMode(Play);
for (client => marble in this.clientMarbles)
marble.setMode(Play);
}
}
}
}
@ -897,26 +955,46 @@ class MarbleWorld extends Scheduler {
});
}
public function addMarble(marble:Marble, onFinish:Void->Void) {
this.marbles.push(marble);
public function addMarble(marble:Marble, client:ClientConnection, onFinish:Void->Void) {
marble.level = cast this;
if (marble.controllable) {
marble.init(cast this, () -> {
marble.init(cast this, client, () -> {
this.scene.addChild(marble.camera);
this.marble = marble;
// Ugly hack
// sky.follow = marble;
sky.follow = marble.camera;
this.collisionWorld.addMovingEntity(marble.collider);
this.collisionWorld.addMarbleEntity(marble.collider);
this.scene.addChild(marble);
onFinish();
});
} else {
this.collisionWorld.addMovingEntity(marble.collider);
this.scene.addChild(marble);
marble.init(cast this, client, () -> {
marble.collisionWorld = this.collisionWorld;
this.collisionWorld.addMovingEntity(marble.collider);
this.collisionWorld.addMarbleEntity(marble.collider);
this.scene.addChild(marble);
if (client != null)
clientMarbles.set(client, marble);
onFinish();
});
}
}
public function addGhostMarble(onFinish:Marble->Void) {
var marb = new Marble();
marb.controllable = false;
marb.init(null, null, () -> {
marb.collisionWorld = this.collisionWorld;
this.collisionWorld.addMovingEntity(marb.collider);
this.collisionWorld.addMarbleEntity(marb.collider);
this.scene.addChild(marb);
onFinish(marb);
});
return marb;
}
public function performRestart() {
this.respawnPressedTime = timeState.timeSinceLoad;
this.restart();
@ -935,11 +1013,53 @@ class MarbleWorld extends Scheduler {
}
}
public function rollback(t:Float) {
var newT = timeState.currentAttemptTime - t;
var rewindFrame = rewindManager.getNextRewindFrame(timeState.currentAttemptTime - t);
rewindManager.applyFrame(rewindFrame);
this.isReplayingMovement = true;
this.currentInputMoves = this.inputRecorder.getMovesFrom(timeState.currentAttemptTime);
}
public function advanceWorld(dt:Float) {
ProfilerUI.measure("updateTimer");
this.updateTimer(dt);
this.tickSchedule(timeState.currentAttemptTime);
if (Key.isDown(Settings.controlsSettings.blast)
|| (MarbleGame.instance.touchInput.blastbutton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.blast)
&& !this.isWatching
&& this.game == "ultra") {
this.marble.useBlast();
}
this.updateGameState();
this.updateBlast(timeState);
ProfilerUI.measure("updateDTS");
for (obj in dtsObjects) {
obj.update(timeState);
}
for (obj in triggers) {
obj.update(timeState);
}
ProfilerUI.measure("updateMarbles");
marble.update(timeState, collisionWorld, this.pathedInteriors);
for (client => marble in clientMarbles) {
marble.update(timeState, collisionWorld, this.pathedInteriors);
}
}
public function update(dt:Float) {
if (!_ready) {
return;
}
if (Key.isPressed(Key.T)) {
rollback(0.4);
}
var realDt = dt;
if ((Key.isDown(Settings.controlsSettings.rewind)
@ -996,9 +1116,34 @@ class MarbleWorld extends Scheduler {
rewindManager.applyFrame(rframe);
}
}
if (dt < 0)
return;
if (this.isReplayingMovement) {
while (this.currentInputMoves.length > 1) {
while (this.currentInputMoves[1].time <= timeState.currentAttemptTime) {
this.currentInputMoves = this.currentInputMoves.slice(1);
if (this.currentInputMoves.length == 1)
break;
}
if (this.currentInputMoves.length > 1) {
dt = this.currentInputMoves[1].time - this.currentInputMoves[0].time;
}
if (this.isReplayingMovement) {
if (this.timeState.currentAttemptTime != this.currentInputMoves[0].time)
trace("fucked");
}
if (this.currentInputMoves.length > 1) {
advanceWorld(dt);
// trace('Position: ${@:privateAccess marble.newPos.sub(currentInputMoves[1].pos).length()}. Vel: ${marble.velocity.sub(currentInputMoves[1].velocity).length()}');
}
}
this.isReplayingMovement = false;
}
ProfilerUI.measure("updateTimer");
this.updateTimer(dt);
@ -1057,8 +1202,14 @@ class MarbleWorld extends Scheduler {
for (obj in triggers) {
obj.update(timeState);
}
if (!isReplayingMovement) {
inputRecorder.recordInput(timeState.currentAttemptTime);
}
ProfilerUI.measure("updateMarbles");
for (marble in marbles) {
marble.update(timeState, collisionWorld, this.pathedInteriors);
for (client => marble in clientMarbles) {
marble.update(timeState, collisionWorld, this.pathedInteriors);
}
_cubemapNeedsUpdate = true;
@ -1096,6 +1247,10 @@ class MarbleWorld extends Scheduler {
if (!this.rewinding && Settings.optionsSettings.rewindEnabled)
this.rewindManager.recordFrame();
if (!this.isReplayingMovement) {
inputRecorder.recordMarble();
}
this.updateTexts();
}
@ -1177,7 +1332,8 @@ class MarbleWorld extends Scheduler {
if (this.timeState.gameplayClock < 0)
this.gameMode.onTimeExpire();
}
this.timeState.currentAttemptTime += dt;
if (!this.isMultiplayer || this.multiplayerStarted)
this.timeState.currentAttemptTime += dt;
} else {
this.timeState.currentAttemptTime = this.replay.currentPlaybackFrame.time;
this.timeState.gameplayClock = this.replay.currentPlaybackFrame.clockTime;
@ -1834,10 +1990,10 @@ class MarbleWorld extends Scheduler {
pathedInteriors.dispose();
}
pathedInteriors = null;
for (marble in this.marbles) {
for (client => marble in clientMarbles) {
marble.dispose();
}
marbles = null;
clientMarbles = null;
for (dtsObject in this.dtsObjects) {
dtsObject.dispose();
}

View file

@ -582,7 +582,7 @@ class PreviewWorld extends Scheduler {
public function spawnMarble(onFinish:Marble->Void) {
var marb = new Marble();
marb.controllable = false;
marb.init(null, () -> {
marb.init(null, null, () -> {
marb.collisionWorld = this.collisionWorld;
this.collisionWorld.addMovingEntity(marb.collider);
this.scene.addChild(marb);

View file

@ -13,6 +13,8 @@ class CollisionWorld {
public var dynamicEntities:Array<CollisionEntity> = [];
public var dynamicOctree:Octree;
var marbleEntities:Array<SphereCollisionEntity> = [];
var dynamicEntitySet:Map<CollisionEntity, Bool> = [];
public function new() {
@ -55,6 +57,13 @@ class CollisionWorld {
contacts = contacts.concat(obj.sphereIntersection(spherecollision, timeState));
}
}
for (marb in marbleEntities) {
if (marb != spherecollision) {
if (spherecollision.go.isCollideable)
contacts = contacts.concat(marb.sphereIntersection(spherecollision, timeState));
}
}
return {foundEntities: foundEntities, contacts: contacts};
}
@ -114,6 +123,14 @@ class CollisionWorld {
// [entity.boundingBox.xSize, entity.boundingBox.ySize, entity.boundingBox.zSize], entity);
}
public function addMarbleEntity(entity:SphereCollisionEntity) {
this.marbleEntities.push(entity);
}
public function removeMarbleEntity(entity:SphereCollisionEntity) {
this.marbleEntities.remove(entity);
}
public function addMovingEntity(entity:CollisionEntity) {
this.dynamicEntities.push(entity);
this.dynamicOctree.insert(entity);

View file

@ -98,17 +98,17 @@ class SphereCollisionEntity extends CollisionEntity {
contact.penetration = radius - (position.sub(contact.point).dot(contact.normal));
contacts.push(contact);
// var othercontact = new CollisionInfo();
// 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.contactDistance = contact.point.distance(position);
// othercontact.force = 0;
// othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal));
// this.marble.queueCollision(othercontact);
var othercontact = new CollisionInfo();
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.contactDistance = contact.point.distance(position);
othercontact.force = 0;
othercontact.penetration = this.radius - (thispos.sub(othercontact.point).dot(othercontact.normal));
this.marble.queueCollision(othercontact);
}
return contacts;
}

View file

@ -66,6 +66,9 @@ class MainMenuGui extends GuiImage {
btnList.addButton(0, "Single Player Game", (sender) -> {
cast(this.parent, Canvas).setContent(new DifficultySelectGui());
});
btnList.addButton(0, "Multiplayer Game", (sender) -> {
cast(this.parent, Canvas).setContent(new MultiplayerGui());
});
// btnList.addButton(2, "Leaderboards", (e) -> {}, 20);
btnList.addButton(2, "Achievements", (e) -> {
cast(this.parent, Canvas).setContent(new AchievementsGui());

106
src/gui/MultiplayerGui.hx Normal file
View file

@ -0,0 +1,106 @@
package gui;
import net.Net;
import src.MarbleGame;
import hxd.res.BitmapFont;
import h3d.Vector;
import src.ResourceLoader;
import src.Settings;
import src.Util;
class MultiplayerGui extends GuiImage {
var innerCtrl:GuiControl;
var btnList:GuiXboxList;
public function new() {
var res = ResourceLoader.getImage("data/ui/xbox/BG_fadeOutSoftEdge.png").resource.toTile();
super(res);
var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt");
var domcasual32b = new BitmapFont(domcasual32fontdata.entry);
@:privateAccess domcasual32b.loader = ResourceLoader.loader;
var domcasual32 = domcasual32b.toSdfFont(cast 42 * Settings.uiScale, MultiChannel);
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
#if hl
var scene2d = hxd.Window.getInstance();
#end
#if js
var scene2d = MarbleGame.instance.scene2d;
#end
var offsetX = (scene2d.width - 1280) / 2;
var offsetY = (scene2d.height - 720) / 2;
var subX = 640 - (scene2d.width - offsetX) * 640 / scene2d.width;
var subY = 480 - (scene2d.height - offsetY) * 480 / scene2d.height;
innerCtrl = new GuiControl();
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
innerCtrl.horizSizing = Width;
innerCtrl.vertSizing = Height;
this.addChild(innerCtrl);
var coliseumfontdata = ResourceLoader.getFileEntry("data/font/ColiseumRR.fnt");
var coliseumb = new BitmapFont(coliseumfontdata.entry);
@:privateAccess coliseumb.loader = ResourceLoader.loader;
var coliseum = coliseumb.toSdfFont(cast 44 * Settings.uiScale, MultiChannel);
var rootTitle = new GuiText(coliseum);
rootTitle.position = new Vector(100, 30);
rootTitle.extent = new Vector(1120, 80);
rootTitle.text.textColor = 0xFFFFFF;
rootTitle.text.text = "MULTIPLAYER";
rootTitle.text.alpha = 0.5;
innerCtrl.addChild(rootTitle);
var btnList = new GuiXboxList();
btnList.position = new Vector(70 - offsetX, 165);
btnList.horizSizing = Left;
btnList.extent = new Vector(502, 500);
innerCtrl.addChild(btnList);
btnList.addButton(3, 'Create Game', (e) -> {
MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(true));
Net.hostServer();
});
btnList.addButton(3, 'Join Game', (e) -> {
Net.joinServer(() -> {
MarbleGame.canvas.setContent(new MultiplayerLevelSelectGui(false));
});
});
var bottomBar = new GuiControl();
bottomBar.position = new Vector(0, 590);
bottomBar.extent = new Vector(640, 200);
bottomBar.horizSizing = Width;
bottomBar.vertSizing = Bottom;
innerCtrl.addChild(bottomBar);
var backButton = new GuiXboxButton("Back", 160);
backButton.position = new Vector(400, 0);
backButton.vertSizing = Bottom;
backButton.horizSizing = Right;
backButton.gamepadAccelerator = ["B"];
backButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE];
backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new MainMenuGui());
bottomBar.addChild(backButton);
}
override function onResize(width:Int, height:Int) {
var offsetX = (width - 1280) / 2;
var offsetY = (height - 720) / 2;
var subX = 640 - (width - offsetX) * 640 / width;
var subY = 480 - (height - offsetY) * 480 / height;
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
super.onResize(width, height);
}
}

View file

@ -0,0 +1,298 @@
package gui;
import net.NetCommands;
import modes.GameMode.ScoreType;
import src.Util;
import haxe.io.Path;
import h2d.filter.DropShadow;
import src.MarbleGame;
import gui.GuiControl.MouseState;
import hxd.res.BitmapFont;
import h3d.Vector;
import src.ResourceLoader;
import src.Settings;
import src.MissionList;
class MultiplayerLevelSelectGui extends GuiImage {
static var currentSelectionStatic:Int = 0;
static var setLevelFn:Int->Void;
static var playSelectedLevel:Void->Void;
var innerCtrl:GuiControl;
public function new(isHost:Bool) {
var res = ResourceLoader.getImage("data/ui/game/CloudBG.jpg").resource.toTile();
super(res);
var arial14fontdata = ResourceLoader.getFileEntry("data/font/Arial Bold.fnt");
var arial14b = new BitmapFont(arial14fontdata.entry);
@:privateAccess arial14b.loader = ResourceLoader.loader;
var arial14 = arial14b.toSdfFont(cast 21 * Settings.uiScale, h2d.Font.SDFChannel.MultiChannel);
function mlFontLoader(text:String) {
return arial14;
}
MarbleGame.instance.toRecord = false;
var fadeEdge = new GuiImage(ResourceLoader.getResource("data/ui/xbox/BG_fadeOutSoftEdge.png", ResourceLoader.getImage, this.imageResources).toTile());
fadeEdge.position = new Vector(0, 0);
fadeEdge.extent = new Vector(640, 480);
fadeEdge.vertSizing = Height;
fadeEdge.horizSizing = Width;
this.addChild(fadeEdge);
var loadAnim = new GuiLoadAnim();
loadAnim.position = new Vector(610, 253);
loadAnim.extent = new Vector(63, 63);
loadAnim.horizSizing = Center;
loadAnim.vertSizing = Bottom;
this.addChild(loadAnim);
var loadTextBg = new GuiText(arial14);
loadTextBg.position = new Vector(608, 335);
loadTextBg.extent = new Vector(63, 40);
loadTextBg.horizSizing = Center;
loadTextBg.vertSizing = Bottom;
loadTextBg.justify = Center;
loadTextBg.text.text = "Loading";
loadTextBg.text.textColor = 0;
this.addChild(loadTextBg);
var loadText = new GuiText(arial14);
loadText.position = new Vector(610, 334);
loadText.extent = new Vector(63, 40);
loadText.horizSizing = Center;
loadText.vertSizing = Bottom;
loadText.justify = Center;
loadText.text.text = "Loading";
this.addChild(loadText);
var difficultyMissions = MissionList.missionList['ultra']["multiplayer"];
if (currentSelectionStatic >= difficultyMissions.length)
currentSelectionStatic = 0;
var curMission = difficultyMissions[currentSelectionStatic];
var lock = true;
var currentToken = 0;
var requestToken = 0;
// var misFile = Path.withoutExtension(Path.withoutDirectory(curMission.path));
// MarbleGame.instance.setPreviewMission(misFile, () -> {
// lock = false;
// if (currentToken != requestToken)
// return;
// this.bmp.visible = false;
// loadAnim.anim.visible = false;
// loadText.text.visible = false;
// loadTextBg.text.visible = false;
// });
var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt");
var domcasual32b = new BitmapFont(domcasual32fontdata.entry);
@:privateAccess domcasual32b.loader = ResourceLoader.loader;
var domcasual32 = domcasual32b.toSdfFont(cast 42 * Settings.uiScale, MultiChannel);
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
#if hl
var scene2d = hxd.Window.getInstance();
#end
#if js
var scene2d = MarbleGame.instance.scene2d;
#end
var offsetX = (scene2d.width - 1280) / 2;
var offsetY = (scene2d.height - 720) / 2;
var subX = 640 - (scene2d.width - offsetX) * 640 / scene2d.width;
var subY = 480 - (scene2d.height - offsetY) * 480 / scene2d.height;
innerCtrl = new GuiControl();
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
innerCtrl.horizSizing = Width;
innerCtrl.vertSizing = Height;
this.addChild(innerCtrl);
var coliseumfontdata = ResourceLoader.getFileEntry("data/font/ColiseumRR.fnt");
var coliseumb = new BitmapFont(coliseumfontdata.entry);
@:privateAccess coliseumb.loader = ResourceLoader.loader;
var coliseum = coliseumb.toSdfFont(cast 44 * Settings.uiScale, MultiChannel);
var rootTitle = new GuiText(coliseum);
rootTitle.position = new Vector(100, 30);
rootTitle.extent = new Vector(1120, 80);
rootTitle.text.textColor = 0xFFFFFF;
rootTitle.text.text = "SELECT LEVEL";
rootTitle.text.alpha = 0.5;
innerCtrl.addChild(rootTitle);
var bottomBar = new GuiControl();
bottomBar.position = new Vector(0, 590);
bottomBar.extent = new Vector(640, 200);
bottomBar.horizSizing = Width;
bottomBar.vertSizing = Bottom;
innerCtrl.addChild(bottomBar);
var backButton = new GuiXboxButton("Back", 160);
backButton.position = new Vector(400, 0);
backButton.vertSizing = Bottom;
backButton.horizSizing = Right;
backButton.gamepadAccelerator = ["B"];
backButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE];
backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new DifficultySelectGui());
bottomBar.addChild(backButton);
// var lbButton = new GuiXboxButton("Leaderboard", 220);
// lbButton.position = new Vector(750, 0);
// lbButton.vertSizing = Bottom;
// lbButton.horizSizing = Right;
// bottomBar.addChild(lbButton);
if (isHost) {
var nextButton = new GuiXboxButton("Play", 160);
nextButton.position = new Vector(960, 0);
nextButton.vertSizing = Bottom;
nextButton.horizSizing = Right;
nextButton.gamepadAccelerator = ["A"];
nextButton.accelerators = [hxd.Key.ENTER];
nextButton.pressedAction = (e) -> {
NetCommands.playLevel();
};
bottomBar.addChild(nextButton);
}
playSelectedLevel = () -> {
MarbleGame.instance.playMission(curMission, true);
}
var levelWnd = new GuiImage(ResourceLoader.getResource("data/ui/xbox/levelPreviewWindow.png", ResourceLoader.getImage, this.imageResources).toTile());
levelWnd.position = new Vector(555, 469);
levelWnd.extent = new Vector(535, 137);
levelWnd.vertSizing = Bottom;
levelWnd.horizSizing = Right;
innerCtrl.addChild(levelWnd);
var statIcon = new GuiImage(ResourceLoader.getResource("data/ui/xbox/statIcon.png", ResourceLoader.getImage, this.imageResources).toTile());
statIcon.position = new Vector(29, 54);
statIcon.extent = new Vector(20, 20);
levelWnd.addChild(statIcon);
var eggIcon = new GuiImage(ResourceLoader.getResource("data/ui/xbox/eggIcon.png", ResourceLoader.getImage, this.imageResources).toTile());
eggIcon.position = new Vector(29, 79);
eggIcon.extent = new Vector(20, 20);
levelWnd.addChild(eggIcon);
var c0 = 0xEBEBEB;
var c1 = 0x8DFF8D;
var c2 = 0x88BCEE;
var c3 = 0xFF7575;
var levelInfoLeft = new GuiMLText(arial14, mlFontLoader);
levelInfoLeft.position = new Vector(69, 54);
levelInfoLeft.extent = new Vector(180, 100);
levelInfoLeft.text.text = '<p align="right"><font color="#EBEBEB">My Best Time:</font><br/><font color="#EBEBEB">Par Time:</font></p>';
levelInfoLeft.text.lineSpacing = 6;
levelWnd.addChild(levelInfoLeft);
var levelInfoMid = new GuiMLText(arial14, mlFontLoader);
levelInfoMid.position = new Vector(269, 54);
levelInfoMid.extent = new Vector(180, 100);
levelInfoMid.text.text = '<p align="left"><font color="#EBEBEB">None</font><br/><font color="#88BCEE">99:59:99</font></p>';
levelInfoMid.text.lineSpacing = 6;
levelWnd.addChild(levelInfoMid);
var levelInfoRight = new GuiMLText(arial14, mlFontLoader);
levelInfoRight.position = new Vector(379, 54);
levelInfoRight.extent = new Vector(180, 100);
levelInfoRight.text.text = '<p align="left"><font color="#EBEBEB">Level 1<br/>Difficulty 1</font></p>';
levelInfoRight.text.lineSpacing = 6;
levelWnd.addChild(levelInfoRight);
var levelNames = difficultyMissions.map(x -> x.title);
var levelSelectOpts = new GuiXboxOptionsList(6, "Level", levelNames);
function setLevel(idx:Int) {
// if (lock)
// return false;
levelSelectOpts.currentOption = idx;
this.bmp.visible = true;
loadAnim.anim.visible = true;
loadText.text.visible = true;
loadTextBg.text.visible = true;
lock = true;
curMission = difficultyMissions[idx];
currentSelectionStatic = idx;
currentToken++;
var misFile = Path.withoutExtension(Path.withoutDirectory(curMission.path));
var mis = difficultyMissions[idx];
var requestToken = currentToken;
if (Settings.easterEggs.exists(mis.path))
eggIcon.bmp.visible = true;
else
eggIcon.bmp.visible = false;
MarbleGame.instance.setPreviewMission(misFile, () -> {
lock = false;
if (requestToken != currentToken)
return;
this.bmp.visible = false;
loadAnim.anim.visible = false;
loadText.text.visible = false;
loadTextBg.text.visible = false;
});
var scoreType = mis.missionInfo.gamemode != null
&& mis.missionInfo.gamemode.toLowerCase() == 'scrum' ? ScoreType.Score : ScoreType.Time;
var myScore = Settings.getScores(mis.path);
var scoreDisp = "None";
if (myScore.length != 0)
scoreDisp = scoreType == Time ? Util.formatTime(myScore[0].time) : Util.formatScore(myScore[0].time);
var isPar = myScore.length != 0 && myScore[0].time < mis.qualifyTime;
var scoreColor = "#EBEBEB";
if (isPar)
scoreColor = "#8DFF8D";
if (scoreType == Score && myScore.length == 0)
scoreColor = "#EBEBEB";
if (scoreType == Time) {
levelInfoLeft.text.text = '<p align="right"><font color="#EBEBEB">My Best Time:</font><br/><font color="#EBEBEB">Par Time:</font></p>';
levelInfoMid.text.text = '<p align="left"><font color="${scoreColor}">${scoreDisp}</font><br/><font color="#88BCEE">${Util.formatTime(mis.qualifyTime)}</font></p>';
}
if (scoreType == Score) {
levelInfoLeft.text.text = '<p align="right"><font color="#EBEBEB">My Best Score:</font></p>';
levelInfoMid.text.text = '<p align="left"><font color="${scoreColor}">${scoreDisp}</font></p>';
}
levelInfoRight.text.text = '<p align="left"><font color="#EBEBEB">Level ${mis.missionInfo.level}<br/>Difficulty ${mis.missionInfo.difficulty == null ? "" : mis.missionInfo.difficulty}</font></p>';
return true;
}
setLevelFn = setLevel;
levelSelectOpts.position = new Vector(380, 435);
levelSelectOpts.extent = new Vector(815, 94);
levelSelectOpts.vertSizing = Bottom;
levelSelectOpts.horizSizing = Right;
levelSelectOpts.alwaysActive = true;
levelSelectOpts.onChangeFunc = (i) -> {
NetCommands.setLobbyLevelIndex(i);
return true;
};
levelSelectOpts.setCurrentOption(currentSelectionStatic);
setLevel(currentSelectionStatic);
innerCtrl.addChild(levelSelectOpts);
}
override function onResize(width:Int, height:Int) {
var offsetX = (width - 1280) / 2;
var offsetY = (height - 720) / 2;
var subX = 640 - (width - offsetX) * 640 / width;
var subY = 480 - (height - offsetY) * 480 / height;
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
super.onResize(width, height);
}
}

244
src/net/Net.hx Normal file
View file

@ -0,0 +1,244 @@
package net;
import haxe.Json;
import datachannel.RTCPeerConnection;
import datachannel.RTCDataChannel;
import hx.ws.WebSocket;
import src.Console;
import net.NetCommands;
enum abstract GameplayState(Int) from Int to Int {
var UNKNOWN;
var LOBBY;
var GAME;
}
enum abstract NetPacketType(Int) from Int to Int {
var NullPacket;
var ClientIdAssign;
var NetCommand;
var Ping;
var PingBack;
}
@:publicFields
class ClientConnection {
var id:Int;
var socket:RTCPeerConnection;
var datachannel:RTCDataChannel;
var state:GameplayState;
var rtt:Float;
var pingSendTime:Float;
var _rttRecords:Array<Float> = [];
public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel) {
this.socket = socket;
this.datachannel = datachannel;
this.id = id;
this.state = GameplayState.LOBBY;
this.rtt = 0;
}
public function ready() {
state = GameplayState.GAME;
}
}
class Net {
static var client:RTCPeerConnection;
static var clientDatachannel:RTCDataChannel;
static var masterWs:WebSocket;
public static var isMP:Bool;
public static var isHost:Bool;
public static var isClient:Bool;
public static var startMP:Bool;
public static var clientId:Int;
public static var networkRNG:Float;
public static var clients:Map<RTCPeerConnection, ClientConnection> = [];
public static var clientIdMap:Map<Int, ClientConnection> = [];
public static function hostServer() {
// host = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0");
// host.bind("127.0.0.1", 28000, (c) -> {
// onClientConnect(c);
// isMP = true;
// });
isHost = true;
isClient = false;
clientId = 0;
masterWs = new WebSocket("ws://localhost:8080");
masterWs.onmessage = (m) -> {
switch (m) {
case StrMessage(content):
var conts = Json.parse(content);
var peer = new RTCPeerConnection(["stun.l.google.com:19302"], "0.0.0.0");
peer.setRemoteDescription(conts.sdp, conts.type);
var candidates = [];
peer.onLocalCandidate = (c) -> {
if (c != "")
candidates.push('a=${c}');
}
peer.onGatheringStateChange = (s) -> {
if (s == RTC_GATHERING_COMPLETE) {
var sdpObj = StringTools.trim(peer.localDescription);
sdpObj = sdpObj + '\r\n' + candidates.join('\r\n');
masterWs.send(Json.stringify({
type: "connect",
sdpObj: {
sdp: sdpObj,
type: "offer"
}
}));
}
}
peer.onDataChannel = (dc) -> {
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");
var candidates = [];
client.onLocalCandidate = (c) -> {
if (c != "")
candidates.push('a=${c}');
}
client.onGatheringStateChange = (s) -> {
if (s == RTC_GATHERING_COMPLETE) {
var sdpObj = StringTools.trim(client.localDescription);
sdpObj = sdpObj + '\r\n' + candidates.join('\r\n');
masterWs.send(Json.stringify({
type: "connect",
sdpObj: {
sdp: sdpObj,
type: "offer"
}
}));
}
}
masterWs.onmessage = (m) -> {
switch (m) {
case StrMessage(content):
var conts = Json.parse(content);
client.setRemoteDescription(conts.sdp, conts.type);
case _: {}
}
}
clientDatachannel = client.createDatachannel("mp");
clientDatachannel.onOpen = (n) -> {
clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0
clientIdMap[0] = clients[client];
onConnectedToServer();
haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation
}
clientDatachannel.onMessage = (b) -> {
onPacketReceived(client, clientDatachannel, new haxe.io.BytesInput(b));
}
isMP = true;
isHost = false;
isClient = true;
}
static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel) {
clientId += 1;
clients.set(c, new ClientConnection(clientId, c, dc));
clientIdMap[clientId] = clients[c];
dc.onMessage = (msgBytes) -> {
onPacketReceived(c, dc, new haxe.io.BytesInput(msgBytes));
}
var b = haxe.io.Bytes.alloc(3);
b.set(0, ClientIdAssign);
b.setUInt16(1, clientId);
dc.sendBytes(b);
Console.log("Client has connected!");
// Send the ping packet to calculcate the RTT
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
clients[c].pingSendTime = Sys.time();
dc.sendBytes(b);
Console.log("Sending ping packet!");
}
static function onConnectedToServer() {
Console.log("Connected to the server!");
// Send the ping packet to calculate the RTT
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
clients[client].pingSendTime = Sys.time();
clientDatachannel.sendBytes(b);
Console.log("Sending ping packet!");
}
static function onPacketReceived(c:RTCPeerConnection, dc:RTCDataChannel, input:haxe.io.BytesInput) {
var packetType = input.readByte();
switch (packetType) {
case NetCommand:
NetCommands.readPacket(input);
case ClientIdAssign:
clientId = input.readUInt16();
Console.log('Client ID set to ${clientId}');
case Ping:
var pingLeft = input.readByte();
Console.log("Got ping packet!");
var b = haxe.io.Bytes.alloc(2);
b.set(0, PingBack);
b.set(1, pingLeft);
dc.sendBytes(b);
case PingBack:
var pingLeft = input.readByte();
Console.log("Got pingback packet!");
var conn = clients[c];
var now = Sys.time();
conn._rttRecords.push((now - conn.pingSendTime));
if (pingLeft > 0) {
conn.pingSendTime = now;
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, pingLeft - 1);
dc.sendBytes(b);
} else {
for (r in conn._rttRecords)
conn.rtt += r;
conn.rtt /= conn._rttRecords.length;
Console.log('Got RTT ${conn.rtt} for client ${conn.id}');
}
case _:
trace("unknown command: " + packetType);
}
}
public static function sendPacketToAll(packetData:haxe.io.BytesOutput) {
var bytes = packetData.getBytes();
for (c => v in clients) {
v.datachannel.sendBytes(packetData.getBytes());
}
}
public static function sendPacketToHost(packetData:haxe.io.BytesOutput) {
var bytes = packetData.getBytes();
clientDatachannel.sendBytes(bytes);
}
}

54
src/net/NetCommands.hx Normal file
View file

@ -0,0 +1,54 @@
package net;
import net.Net.GameplayState;
import net.Net.NetPacketType;
import gui.MultiplayerLevelSelectGui;
import src.MarbleGame;
@:build(net.RPCMacro.build())
class NetCommands {
@:rpc(server) public static function setLobbyLevelIndex(i:Int) {
MultiplayerLevelSelectGui.setLevelFn(i);
}
@:rpc(server) public static function playLevel() {
MultiplayerLevelSelectGui.playSelectedLevel();
}
@:rpc(server) public static function setNetworkRNG(rng:Float) {
Net.networkRNG = rng;
if (MarbleGame.instance.world != null) {
var gameMode = MarbleGame.instance.world.gameMode;
if (gameMode is modes.HuntMode) {
var hunt:modes.HuntMode = cast gameMode;
@:privateAccess hunt.rng.setSeed(cast rng);
@:privateAccess hunt.rng2.setSeed(cast rng);
}
}
}
@:rpc(client) public static function clientIsReady(clientId:Int) {
if (Net.isHost) {
Net.clientIdMap[clientId].ready();
var allReady = true;
for (id => client in Net.clientIdMap) {
if (client.state != GameplayState.GAME)
allReady = false;
}
if (allReady) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.allClientsReady();
}
}
}
}
@:rpc(server) public static function setStartTime(t:Float) {
if (MarbleGame.instance.world != null) {
if (Net.isClient) {
t -= Net.clientIdMap[0].rtt / 2; // Subtract receving time
}
MarbleGame.instance.world.startRealTime = MarbleGame.instance.world.timeState.timeSinceLoad + t;
}
}
}

136
src/net/RPCMacro.hx Normal file
View file

@ -0,0 +1,136 @@
package net;
import haxe.macro.Context;
import haxe.macro.Expr;
class RPCMacro {
macro static public function build():Array<Field> {
var fields = Context.getBuildFields();
var rpcFnId = 1;
var idtoFn:Map<Int, {
name:String,
serialize:Array<Expr>,
deserialize:Array<Expr>
}> = new Map();
for (field in fields) {
if (field.meta.length > 0 && field.meta[0].name == ':rpc') {
switch (field.kind) {
case FFun(f):
{
var serializeFns = [];
var deserializeFns = [];
var callExprs = [];
for (arg in f.args) {
var argName = arg.name;
switch (arg.type) {
case TPath({
name: 'Int'
}): {
deserializeFns.push(macro var $argName = stream.readInt32());
callExprs.push(macro $i{argName});
serializeFns.push(macro stream.writeInt32($i{argName}));
}
case TPath({
name: 'Float'
}): {
deserializeFns.push(macro var $argName = stream.readFloat());
callExprs.push(macro $i{argName});
serializeFns.push(macro stream.writeFloat($i{argName}));
}
case _: {}
}
}
deserializeFns.push(macro {
$i{field.name}($a{callExprs});
});
idtoFn.set(rpcFnId, {
name: field.name,
serialize: serializeFns,
deserialize: deserializeFns
});
var directionParam = field.meta[0].params[0].expr;
switch (directionParam) {
case EConst(CIdent("server")):
var lastExpr = macro {
if (Net.isHost) {
var stream = new haxe.io.BytesOutput();
stream.writeByte(NetPacketType.NetCommand);
stream.writeByte($v{rpcFnId});
$b{serializeFns};
Net.sendPacketToAll(stream);
}
};
f.expr = macro $b{[f.expr, lastExpr]};
case EConst(CIdent("client")):
var lastExpr = macro {
if (!Net.isHost) {
var stream = new haxe.io.BytesOutput();
stream.writeByte(NetPacketType.NetCommand);
stream.writeByte($v{rpcFnId});
$b{serializeFns};
Net.sendPacketToHost(stream);
}
};
f.expr = macro $b{[f.expr, lastExpr]};
case _:
{}
}
rpcFnId++;
}
case _:
{}
}
}
}
var cases:Array<Case> = [];
for (k => v in idtoFn) {
cases.push({
values: [macro $v{k}],
expr: macro {
$b{v.deserialize}
}
});
}
var deserializeField:Field = {
name: "readPacket",
pos: Context.currentPos(),
access: [APublic, AStatic],
kind: FFun({
args: [
{
name: "stream",
type: haxe.macro.TypeTools.toComplexType(Context.getType('haxe.io.Input'))
}
],
expr: macro {
var fnId = stream.readByte();
$e{
{
expr: ESwitch(macro fnId, cases, null),
pos: Context.currentPos()
}
}
}
})
};
fields.push(deserializeField);
return fields;
}
}

View file

@ -0,0 +1,62 @@
package rewind;
import src.MarbleWorld;
import h3d.Vector;
import src.Marble.Move;
@:publicFields
class InputRecorderFrame {
var time:Float;
var move:Move;
var marbleAxes:Array<Vector>;
var pos:Vector;
var velocity:Vector;
public function new() {}
}
class InputRecorder {
var frames:Array<InputRecorderFrame>;
var level:MarbleWorld;
public function new(level:MarbleWorld) {
frames = [];
this.level = level;
}
public function recordInput(t:Float) {
var frame = new InputRecorderFrame();
frame.time = t;
frame.move = level.marble.recordMove();
frames.push(frame);
}
public function recordMarble() {
frames[frames.length - 1].pos = @:privateAccess level.marble.newPos?.clone();
frames[frames.length - 1].velocity = level.marble.velocity.clone();
}
public function recordAxis(axis:Array<Vector>) {
frames[frames.length - 1].marbleAxes = axis.copy();
}
public function getMovesFrom(t:Float) {
if (frames.length == 0)
return [];
var start = 0;
var end = frames.length - 1;
var mid = Std.int(frames.length / 2);
while (end - start > 1) {
mid = Std.int((start / 2) + (end / 2));
if (frames[mid].time < t) {
start = mid + 1;
} else if (frames[mid].time > t) {
end = mid - 1;
} else {
start = end = mid;
}
}
return frames.slice(start - 1);
}
}