mirror of
https://github.com/RandomityGuy/MBHaxe.git
synced 2025-10-30 08:11:25 +00:00
begin netcode
This commit is contained in:
parent
1aa6188786
commit
5ab1bb7a65
19 changed files with 1314 additions and 161 deletions
|
|
@ -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
38
server/Signalling.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
5
server/build_signalling.hxml
Normal file
5
server/build_signalling.hxml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
--library hxWebSockets
|
||||
--library datachannel
|
||||
--main Signalling
|
||||
-cp .
|
||||
--hl bin/signalling.hl
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package;
|
||||
|
||||
import datachannel.RTC;
|
||||
import gui.VersionGui;
|
||||
import gui.PresentsGui;
|
||||
import src.Debug;
|
||||
|
|
@ -88,6 +89,7 @@ class Main extends hxd.App {
|
|||
#end
|
||||
|
||||
// try {
|
||||
RTC.init();
|
||||
Http.init();
|
||||
haxe.MainLoop.add(() -> Http.loop());
|
||||
Settings.init();
|
||||
|
|
@ -148,6 +150,7 @@ class Main extends hxd.App {
|
|||
// marbleGame.update(1 / 60);
|
||||
// timeAccumulator -= 1 / 60;
|
||||
// }
|
||||
RTC.processEvents();
|
||||
marbleGame.update(dt);
|
||||
// } catch (e) {
|
||||
// Console.error(e.message);
|
||||
|
|
|
|||
184
src/Marble.hx
184
src/Marble.hx
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -323,14 +323,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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -105,7 +109,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> = [];
|
||||
|
|
@ -183,6 +186,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> = [];
|
||||
|
||||
|
|
@ -207,7 +222,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;
|
||||
|
|
@ -216,6 +231,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() {
|
||||
|
|
@ -266,7 +292,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) {
|
||||
|
|
@ -526,6 +560,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)
|
||||
|
|
@ -585,17 +628,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -896,26 +954,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();
|
||||
|
|
@ -934,11 +1012,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)
|
||||
|
|
@ -995,9 +1115,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);
|
||||
|
||||
|
|
@ -1056,8 +1201,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;
|
||||
|
|
@ -1095,6 +1246,10 @@ class MarbleWorld extends Scheduler {
|
|||
if (!this.rewinding && Settings.optionsSettings.rewindEnabled)
|
||||
this.rewindManager.recordFrame();
|
||||
|
||||
if (!this.isReplayingMovement) {
|
||||
inputRecorder.recordMarble();
|
||||
}
|
||||
|
||||
this.updateTexts();
|
||||
}
|
||||
|
||||
|
|
@ -1176,7 +1331,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;
|
||||
|
|
@ -1833,10 +1989,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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,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
106
src/gui/MultiplayerGui.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
298
src/gui/MultiplayerLevelSelectGui.hx
Normal file
298
src/gui/MultiplayerLevelSelectGui.hx
Normal 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
244
src/net/Net.hx
Normal 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
54
src/net/NetCommands.hx
Normal 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
136
src/net/RPCMacro.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
62
src/rewind/InputRecorder.hx
Normal file
62
src/rewind/InputRecorder.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue