Authoritative server refactor

The high-level multiplayer API in Godot appears
to introduce ways for players to cheat too easily.

As a precaution, players can now hopefully only
send messages to the server, which relays them to
each player, after potentially validating them.

- Player's Name is set to their network ID,
  DisplayName accessible through new property
- Add Authenticating NetworkStatus:
  This is where new players will send data
  about themselves, currently appearance.
- Use _Ready instead of _EnterTree where applicable
- Add Init extension method:
  Allows initializing members before the
  node has been added to the scene tree.
- Add Rpc/RsetExcept extension methods
main
copygirl 5 years ago
parent 44298a707b
commit d93baa7fd0
  1. 21
      scene/EscapeMenu.tscn
  2. 14
      scene/GameScene.tscn
  3. 2
      scene/LocalPlayer.tscn
  4. 8
      scene/Player.tscn
  5. 21
      src/Camera.cs
  6. 2
      src/EscapeMenu.cs
  7. 77
      src/EscapeMenuAppearance.cs
  8. 30
      src/EscapeMenuMultiplayer.cs
  9. 36
      src/Extensions.cs
  10. 18
      src/Game.cs
  11. 42
      src/LocalPlayer.cs
  12. 157
      src/Network.cs
  13. 113
      src/Player.cs
  14. 2
      src/Viewport.cs

@ -7,6 +7,7 @@
[ext_resource path="res://src/EscapeMenuAppearance.cs" type="Script" id=5] [ext_resource path="res://src/EscapeMenuAppearance.cs" type="Script" id=5]
[node name="EscapeMenu" type="Control"] [node name="EscapeMenu" type="Control"]
pause_mode = 2
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
theme = ExtResource( 1 ) theme = ExtResource( 1 )
@ -29,10 +30,10 @@ __meta__ = {
} }
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"] [node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
margin_left = 198.0 margin_left = 518.0
margin_top = 78.0 margin_top = 258.0
margin_right = 441.0 margin_right = 761.0
margin_bottom = 281.0 margin_bottom = 461.0
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"] [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"]
margin_left = 7.0 margin_left = 7.0
@ -73,9 +74,9 @@ margin_top = 27.0
margin_right = -4.0 margin_right = -4.0
margin_bottom = -4.0 margin_bottom = -4.0
script = ExtResource( 5 ) script = ExtResource( 5 )
PlayerNamePath = NodePath("VBoxContainer/ContainerName/Name") DisplayNamePath = NodePath("VBoxContainer/ContainerName/DisplayName")
ColorPreviewPath = NodePath("VBoxContainer/ContainerColor/Preview") ColorPreviewPath = NodePath("VBoxContainer/ContainerColor/Preview")
ColorSliderPath = NodePath("VBoxContainer/ContainerColor/HSlider") ColorSliderPath = NodePath("VBoxContainer/ContainerColor/Hue")
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance"] [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance"]
margin_left = 19.0 margin_left = 19.0
@ -95,7 +96,7 @@ rect_min_size = Vector2( 36, 0 )
text = "Name:" text = "Name:"
align = 2 align = 2
[node name="Name" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerName"] [node name="DisplayName" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerName"]
margin_left = 40.0 margin_left = 40.0
margin_right = 182.0 margin_right = 182.0
margin_bottom = 19.0 margin_bottom = 19.0
@ -124,7 +125,7 @@ margin_right = 56.0
margin_bottom = 16.0 margin_bottom = 16.0
texture = ExtResource( 3 ) texture = ExtResource( 3 )
[node name="HSlider" type="HSlider" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerColor"] [node name="Hue" type="HSlider" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerColor"]
margin_left = 60.0 margin_left = 60.0
margin_right = 182.0 margin_right = 182.0
margin_bottom = 16.0 margin_bottom = 16.0
@ -318,8 +319,8 @@ __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
[connection signal="visibility_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Appearance_visibility_changed"] [connection signal="visibility_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Appearance_visibility_changed"]
[connection signal="text_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerName/Name" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Name_text_changed"] [connection signal="text_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerName/DisplayName" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_DisplayName_text_changed"]
[connection signal="value_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerColor/HSlider" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_HSlider_value_changed"] [connection signal="value_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerColor/Hue" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Hue_value_changed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer/ServerStartStop" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerStartStop_pressed"] [connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer/ServerStartStop" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerStartStop_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient/ClientDisConnect" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ClientDisConnect_pressed"] [connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient/ClientDisConnect" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ClientDisConnect_pressed"]
[connection signal="toggled" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerHideAddress/HideAddress" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_HideAddress_toggled"] [connection signal="toggled" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerHideAddress/HideAddress" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_HideAddress_toggled"]

@ -12,16 +12,21 @@
[node name="Game" type="Node"] [node name="Game" type="Node"]
script = ExtResource( 3 ) script = ExtResource( 3 )
Player = ExtResource( 5 ) LocalPlayerPath = NodePath("Players/LocalPlayer")
Block = ExtResource( 6 ) BlockScene = ExtResource( 6 )
[node name="Viewport" type="Node" parent="."] [node name="Viewport" type="Node" parent="."]
script = ExtResource( 7 ) script = ExtResource( 7 )
[node name="Network" type="Node" parent="."] [node name="Network" type="Node" parent="."]
script = ExtResource( 8 ) script = ExtResource( 8 )
PlayerContainerPath = NodePath("..") PlayerContainerPath = NodePath("../Players")
OtherPlayer = ExtResource( 9 ) OtherPlayerScene = ExtResource( 9 )
[node name="Players" type="Node" parent="."]
[node name="LocalPlayer" parent="Players" instance=ExtResource( 5 )]
position = Vector2( 0, -2 )
[node name="HUD" type="CanvasLayer" parent="."] [node name="HUD" type="CanvasLayer" parent="."]
@ -34,5 +39,4 @@ script = ExtResource( 2 )
texture = ExtResource( 4 ) texture = ExtResource( 4 )
[node name="EscapeMenu" parent="HUD" instance=ExtResource( 1 )] [node name="EscapeMenu" parent="HUD" instance=ExtResource( 1 )]
pause_mode = 2
visible = false visible = false

@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=2] [gd_scene load_steps=3 format=2]
[ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1] [ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Player.cs" type="Script" id=3] [ext_resource path="res://src/LocalPlayer.cs" type="Script" id=3]
[node name="LocalPlayer" instance=ExtResource( 1 )] [node name="LocalPlayer" instance=ExtResource( 1 )]
script = ExtResource( 3 ) script = ExtResource( 3 )

@ -1,7 +1,8 @@
[gd_scene load_steps=4 format=2] [gd_scene load_steps=5 format=2]
[ext_resource path="res://ui_theme.tres" type="Theme" id=1] [ext_resource path="res://ui_theme.tres" type="Theme" id=1]
[ext_resource path="res://gfx/player.png" type="Texture" id=2] [ext_resource path="res://gfx/player.png" type="Texture" id=2]
[ext_resource path="res://src/Player.cs" type="Script" id=3]
[sub_resource type="CircleShape2D" id=1] [sub_resource type="CircleShape2D" id=1]
radius = 8.0 radius = 8.0
@ -9,6 +10,9 @@ radius = 8.0
[node name="Player" type="KinematicBody2D"] [node name="Player" type="KinematicBody2D"]
z_index = 10 z_index = 10
collision_layer = 0 collision_layer = 0
script = ExtResource( 3 )
DisplayNamePath = NodePath("DisplayName")
SpritePath = NodePath("Sprite")
[node name="CircleShape" type="CollisionShape2D" parent="."] [node name="CircleShape" type="CollisionShape2D" parent="."]
shape = SubResource( 1 ) shape = SubResource( 1 )
@ -17,7 +21,7 @@ shape = SubResource( 1 )
z_index = -5 z_index = -5
texture = ExtResource( 2 ) texture = ExtResource( 2 )
[node name="Name" type="Label" parent="."] [node name="DisplayName" type="Label" parent="."]
modulate = Color( 1, 1, 1, 0.501961 ) modulate = Color( 1, 1, 1, 0.501961 )
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5

@ -1,21 +0,0 @@
using Godot;
public class Camera : Camera2D
{
public Cursor Cursor { get; private set; }
public override void _EnterTree()
{
// Cursor = GetViewport().GetNode<Cursor>("Cursor");
}
public override void _Process(float delta)
{
// TODO: Implement some kind of "zoom" mechanic?
// var mousePos = GetTree().Root.GetMousePosition();
// var centerPos = OS.WindowSize / 2;
// var scale = ((Viewport)GetViewport()).Scale;
// Position = !Cursor.Visible ? Vector2.Zero
// : ((mousePos - centerPos) / scale).Clamped(MaxDistance) / 2;
}
}

@ -5,7 +5,7 @@ public class EscapeMenu : Control
[Export] public NodePath ReturnPath { get; set; } [Export] public NodePath ReturnPath { get; set; }
public Button Return { get; private set; } public Button Return { get; private set; }
public override void _EnterTree() public override void _Ready()
{ {
Return = GetNode<Button>(ReturnPath); Return = GetNode<Button>(ReturnPath);
} }

@ -3,73 +3,25 @@ using Godot;
public class EscapeMenuAppearance : CenterContainer public class EscapeMenuAppearance : CenterContainer
{ {
[Export] public NodePath PlayerNamePath { get; set; } [Export] public NodePath DisplayNamePath { get; set; }
[Export] public NodePath ColorPreviewPath { get; set; } [Export] public NodePath ColorPreviewPath { get; set; }
[Export] public NodePath ColorSliderPath { get; set; } [Export] public NodePath ColorSliderPath { get; set; }
public LineEdit PlayerName { get; private set; } public Game Game { get; private set; }
public LineEdit DisplayName { get; private set; }
public TextureRect ColorPreview { get; private set; } public TextureRect ColorPreview { get; private set; }
public Slider ColorSlider { get; private set; } public Slider ColorSlider { get; private set; }
public Network Network { get; private set; } public override void _Ready()
public Player LocalPlayer { get; private set; }
public override void _EnterTree()
{ {
PlayerName = GetNode<LineEdit>(PlayerNamePath); Game = GetNode<Game>("/root/Game");
DisplayName = GetNode<LineEdit>(DisplayNamePath);
ColorPreview = GetNode<TextureRect>(ColorPreviewPath); ColorPreview = GetNode<TextureRect>(ColorPreviewPath);
ColorSlider = GetNode<Slider>(ColorSliderPath); ColorSlider = GetNode<Slider>(ColorSliderPath);
CallDeferred(nameof(Initialize));
}
private void Initialize()
{
Network = GetNode<Network>("/root/Game/Network");
LocalPlayer = GetNode<Player>("/root/Game/LocalPlayer");
ColorSlider.Value = GD.RandRange(0.0, 1.0); ColorSlider.Value = GD.RandRange(0.0, 1.0);
var color = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F); var color = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
LocalPlayer.GetNode<Sprite>("Sprite").Modulate = color; Game.LocalPlayer.Color = ColorPreview.Modulate = color;
ColorPreview.Modulate = color;
Network.Connect(nameof(Network.StatusChanged), this, nameof(OnNetworkStatusChanged));
GetTree().Connect("network_peer_connected", this, nameof(OnPeerConnected));
}
private void OnNetworkStatusChanged(Network.Status status)
{
if (status == Network.Status.ConnectedToServer)
SendAppearance();
}
private void OnPeerConnected(int id)
{
// TODO: See if we can do something with syncing these directly?
var name = LocalPlayer.GetNode<Label>("Name").Text;
var hue = LocalPlayer.GetNode<Sprite>("Sprite").Modulate.h;
RpcId(id, nameof(AppearanceChanged), name, hue);
}
private void SendAppearance()
{
// TODO: See if we can do something with syncing these directly?
var name = LocalPlayer.GetNode<Label>("Name").Text;
var hue = LocalPlayer.GetNode<Sprite>("Sprite").Modulate.h;
Rpc(nameof(AppearanceChanged), name, hue);
}
[Remote]
private void AppearanceChanged(string name, float hue)
{
// TODO: Clear out invalid characters from name.
hue = Mathf.Clamp(hue, 0.0F, 1.0F);
var id = GetTree().GetRpcSenderId();
var player = Network.GetOrCreatePlayerWithId(id);
player.GetNode<Label>("Name").Text = name;
player.GetNode<Sprite>("Sprite").Modulate = Color.FromHsv(hue, 1.0F, 1.0F);
} }
@ -77,17 +29,17 @@ public class EscapeMenuAppearance : CenterContainer
#pragma warning disable IDE1006 #pragma warning disable IDE1006
private static readonly Regex INVALID_CHARS = new Regex(@"\s"); private static readonly Regex INVALID_CHARS = new Regex(@"\s");
private void _on_Name_text_changed(string text) private void _on_DisplayName_text_changed(string text)
{ {
var validText = INVALID_CHARS.Replace(text, ""); var validText = INVALID_CHARS.Replace(text, "");
if (validText != text) { if (validText != text) {
var previousCaretPos = PlayerName.CaretPosition; var previousCaretPos = DisplayName.CaretPosition;
PlayerName.Text = validText; DisplayName.Text = validText;
PlayerName.CaretPosition = previousCaretPos - (text.Length - validText.Length); DisplayName.CaretPosition = previousCaretPos - (text.Length - validText.Length);
} }
} }
private void _on_HSlider_value_changed(float value) private void _on_Hue_value_changed(float value)
{ {
var color = Color.FromHsv(value, 1.0F, 1.0F); var color = Color.FromHsv(value, 1.0F, 1.0F);
ColorPreview.Modulate = color; ColorPreview.Modulate = color;
@ -96,8 +48,7 @@ public class EscapeMenuAppearance : CenterContainer
private void _on_Appearance_visibility_changed() private void _on_Appearance_visibility_changed()
{ {
if (IsVisibleInTree()) return; if (IsVisibleInTree()) return;
LocalPlayer.GetNode<Label>("Name").Text = PlayerName.Text; Game.LocalPlayer.DisplayName = DisplayName.Text;
LocalPlayer.GetNode<Sprite>("Sprite").Modulate = ColorPreview.Modulate; Game.LocalPlayer.Color = ColorPreview.Modulate;
if (GetTree().NetworkPeer != null) SendAppearance();
} }
} }

@ -16,7 +16,7 @@ public class EscapeMenuMultiplayer : Container
public Network Network { get; private set; } public Network Network { get; private set; }
public override void _EnterTree() public override void _Ready()
{ {
Status = GetNode<Label>(StatusPath); Status = GetNode<Label>(StatusPath);
ServerStartStop = GetNode<Button>(ServerStartStopPath); ServerStartStop = GetNode<Button>(ServerStartStopPath);
@ -31,33 +31,39 @@ public class EscapeMenuMultiplayer : Container
} }
private void OnNetworkStatusChanged(Network.Status status) private void OnNetworkStatusChanged(NetworkStatus status)
{ {
switch (status) { switch (status) {
case Network.Status.NoConnection: case NetworkStatus.NoConnection:
Status.Text = "No Connection"; Status.Text = "No Connection";
Status.Modulate = Colors.Red; Status.Modulate = Colors.Red;
break; break;
case Network.Status.ServerRunning: case NetworkStatus.ServerRunning:
Status.Text = "Server Running"; Status.Text = "Server Running";
Status.Modulate = Colors.Green; Status.Modulate = Colors.Green;
break; break;
case Network.Status.Connecting: case NetworkStatus.Connecting:
Status.Text = "Connecting ..."; Status.Text = "Connecting ...";
Status.Modulate = Colors.Yellow; Status.Modulate = Colors.Yellow;
break; break;
case Network.Status.ConnectedToServer: case NetworkStatus.Authenticating:
Status.Text = "Authenticating ...";
Status.Modulate = Colors.YellowGreen;
break;
case NetworkStatus.ConnectedToServer:
Status.Text = "Connected to Server"; Status.Text = "Connected to Server";
Status.Modulate = Colors.Green; Status.Modulate = Colors.Green;
break; break;
} }
ServerPort.Editable = status == Network.Status.NoConnection; var noConnection = status == NetworkStatus.NoConnection;
ServerStartStop.Text = (status == Network.Status.ServerRunning) ? "Stop Server" : "Start Server"; ServerPort.Editable = noConnection;
ClientAddress.Editable = status == Network.Status.NoConnection; ServerStartStop.Disabled = noConnection;
ClientDisConnect.Text = (status < Network.Status.Connecting) ? "Connect" : "Disconnect"; ClientAddress.Editable = noConnection;
ClientDisConnect.Disabled = status == Network.Status.ServerRunning; ServerStartStop.Text = (status == NetworkStatus.ServerRunning) ? "Stop Server" : "Start Server";
if (Visible) GetTree().Paused = status == Network.Status.NoConnection; ClientDisConnect.Text = (status < NetworkStatus.Connecting) ? "Connect" : "Disconnect";
ClientDisConnect.Disabled = status == NetworkStatus.ServerRunning;
if (Visible) GetTree().Paused = noConnection;
} }

@ -2,6 +2,38 @@ using Godot;
public static class Extensions public static class Extensions
{ {
public static void RemoveFromParent(this Node node) public static void RemoveFromParent(this Node @this)
=> node.GetParent().RemoveChild(node); => @this.GetParent().RemoveChild(@this);
public static T Init<T>(this PackedScene @this)
where T : Node
{
var instance = (T)@this.Instance();
(instance as IInitializer)?.Initialize();
return instance;
}
public static void RpcExcept(this Node @this, int except, string method, params object[] args)
{
foreach (var peer in @this.GetTree().GetNetworkConnectedPeers())
if (peer != except) @this.RpcId(peer, method, args);
}
public static void RsetUnreliableExcept(this Node @this, int except, string property, object value)
{
foreach (var peer in @this.GetTree().GetNetworkConnectedPeers())
if (peer != except) @this.RsetUnreliableId(peer, property, value);
}
public static void RsetExcept(this Node @this, int except, string property, object value)
{
foreach (var peer in @this.GetTree().GetNetworkConnectedPeers())
if (peer != except) @this.RsetId(peer, property, value);
}
}
public interface IInitializer
{
void Initialize();
} }

@ -3,28 +3,28 @@ using Godot;
public class Game : Node public class Game : Node
{ {
[Export] public Vector2 RoomSize { get; set; } = new Vector2(32, 18) * 16; [Export] public Vector2 RoomSize { get; set; } = new Vector2(32, 18) * 16;
[Export] public NodePath LocalPlayerPath { get; set; }
[Export] public PackedScene BlockScene { get; set; }
[Export] public PackedScene Player { get; set; } public LocalPlayer LocalPlayer { get; private set; }
[Export] public PackedScene Block { get; set; }
public override void _Ready() // Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
{ {
GD.Randomize(); GD.Randomize();
SpawnPlayer(); LocalPlayer = GetNode<LocalPlayer>(LocalPlayerPath);
SpawnBlocks();
} }
private void SpawnPlayer() public override void _Ready()
{ {
var player = (Player)Player.Instance(); SpawnBlocks();
AddChild(player);
} }
private void SpawnBlocks() private void SpawnBlocks()
{ {
void SpawnBlockAt(int x, int y) void SpawnBlockAt(int x, int y)
{ {
var block = (Node2D)Block.Instance(); var block = BlockScene.Init<Node2D>();
block.Position = new Vector2(x, y); block.Position = new Vector2(x, y);
AddChild(block); AddChild(block);
} }

@ -0,0 +1,42 @@
using Godot;
using System;
public class LocalPlayer : Player
{
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F);
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F);
[Export] public float Speed { get; set; } = 120;
[Export] public float JumpSpeed { get; set; } = 180;
[Export] public float Gravity { get; set; } = 400;
[Export(PropertyHint.Range, "0,1")]
public float Friction { get; set; } = 0.1F;
[Export(PropertyHint.Range, "0,1")]
public float Acceleration { get; set; } = 0.25F;
private Vector2 _velocity = Vector2.Zero;
private DateTime? _jumpPressed = null;
private DateTime? _lastOnFloor = null;
public override void _PhysicsProcess(float delta)
{
var moveDir = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left");
_velocity.x = (moveDir != 0) ? Mathf.Lerp(_velocity.x, moveDir * Speed, Acceleration)
: Mathf.Lerp(_velocity.x, 0, Friction);
_velocity.y += Gravity * delta;
_velocity = MoveAndSlide(_velocity, Vector2.Up);
if (Input.IsActionJustPressed("move_jump"))
_jumpPressed = DateTime.Now;
if (IsOnFloor())
_lastOnFloor = DateTime.Now;
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) &&
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) {
_velocity.y = -JumpSpeed;
_jumpPressed = null;
_lastOnFloor = null;
}
}
}

@ -1,32 +1,35 @@
using Godot; using Godot;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
public enum NetworkStatus
{
NoConnection,
ServerRunning,
Connecting,
Authenticating,
ConnectedToServer,
}
public class Network : Node public class Network : Node
{ {
public enum Status private readonly Dictionary<int, Player> _playersById = new Dictionary<int, Player>();
{
NoConnection,
ServerRunning,
Connecting,
ConnectedToServer,
}
[Export] public ushort DefaultPort { get; set; } = 42005; [Export] public ushort DefaultPort { get; set; } = 42005;
[Export] public string DefaultAddress { get; set; } = "localhost"; [Export] public string DefaultAddress { get; set; } = "localhost";
[Export] public NodePath PlayerContainerPath { get; set; } [Export] public NodePath PlayerContainerPath { get; set; }
[Export] public PackedScene OtherPlayer { get; set; } [Export] public PackedScene OtherPlayerScene { get; set; }
public Game Game { get; private set; }
public Node PlayerContainer { get; private set; } public Node PlayerContainer { get; private set; }
public Player LocalPlayer { get; private set; } public NetworkStatus Status { get; private set; } = NetworkStatus.NoConnection;
public Status CurrentStatus { get; private set; } = Status.NoConnection; [Signal] public delegate void StatusChanged(NetworkStatus status);
[Signal] public delegate void StatusChanged(Status status);
public override void _Ready() public override void _Ready()
{ {
Game = GetNode<Game>("/root/Game");
PlayerContainer = GetNode(PlayerContainerPath); PlayerContainer = GetNode(PlayerContainerPath);
GetTree().Connect("connected_to_server", this, nameof(OnClientConnected)); GetTree().Connect("connected_to_server", this, nameof(OnClientConnected));
@ -37,10 +40,17 @@ public class Network : Node
GetTree().Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected)); GetTree().Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected));
} }
public override void _Process(float delta)
public Player GetPlayer(int id)
=> _playersById.TryGetValue(id, out var value) ? value : null;
public void ClearPlayers()
{ {
if (LocalPlayer == null) return; Game.LocalPlayer.NetworkId = -1;
RpcUnreliable(nameof(OnPlayerMoved), LocalPlayer.Position); foreach (var player in _playersById.Values)
if (!player.IsLocal)
player.RemoveFromParent();
_playersById.Clear();
} }
@ -49,14 +59,15 @@ public class Network : Node
if (GetTree().NetworkPeer != null) throw new InvalidOperationException(); if (GetTree().NetworkPeer != null) throw new InvalidOperationException();
var peer = new NetworkedMultiplayerENet(); var peer = new NetworkedMultiplayerENet();
// TODO: Somehow show there was an error.
var error = peer.CreateServer(port); var error = peer.CreateServer(port);
if (error != Error.Ok) return error; if (error != Error.Ok) return error;
GetTree().NetworkPeer = peer; GetTree().NetworkPeer = peer;
LocalPlayer = FindLocalPlayer();
CurrentStatus = Status.ServerRunning; Status = NetworkStatus.ServerRunning;
EmitSignal(nameof(StatusChanged), CurrentStatus); EmitSignal(nameof(StatusChanged), Status);
Game.LocalPlayer.NetworkId = 1;
_playersById.Add(1, Game.LocalPlayer);
return Error.Ok; return Error.Ok;
} }
@ -65,16 +76,13 @@ public class Network : Node
{ {
if ((GetTree().NetworkPeer == null) || !GetTree().IsNetworkServer()) throw new InvalidOperationException(); if ((GetTree().NetworkPeer == null) || !GetTree().IsNetworkServer()) throw new InvalidOperationException();
// TODO: Disconnect players gracefully.
((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection(); ((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection();
GetTree().NetworkPeer = null; GetTree().NetworkPeer = null;
LocalPlayer = null; Status = NetworkStatus.NoConnection;
foreach (var player in GetOtherPlayers()) EmitSignal(nameof(StatusChanged), Status);
player.RemoveFromParent();
CurrentStatus = Status.NoConnection; ClearPlayers();
EmitSignal(nameof(StatusChanged), CurrentStatus);
} }
public Error ConnectToServer(string address, ushort port) public Error ConnectToServer(string address, ushort port)
@ -82,13 +90,12 @@ public class Network : Node
if (GetTree().NetworkPeer != null) throw new InvalidOperationException(); if (GetTree().NetworkPeer != null) throw new InvalidOperationException();
var peer = new NetworkedMultiplayerENet(); var peer = new NetworkedMultiplayerENet();
// TODO: Somehow show there was an error.
var error = peer.CreateClient(address, port); var error = peer.CreateClient(address, port);
if (error != Error.Ok) return error; if (error != Error.Ok) return error;
GetTree().NetworkPeer = peer; GetTree().NetworkPeer = peer;
CurrentStatus = Status.Connecting; Status = NetworkStatus.Connecting;
EmitSignal(nameof(StatusChanged), CurrentStatus); EmitSignal(nameof(StatusChanged), Status);
return Error.Ok; return Error.Ok;
} }
@ -97,64 +104,86 @@ public class Network : Node
{ {
if ((GetTree().NetworkPeer == null) || GetTree().IsNetworkServer()) throw new InvalidOperationException(); if ((GetTree().NetworkPeer == null) || GetTree().IsNetworkServer()) throw new InvalidOperationException();
// TODO: Disconnect from server gracefully.
((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection(); ((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection();
GetTree().NetworkPeer = null; GetTree().NetworkPeer = null;
LocalPlayer = null; Status = NetworkStatus.NoConnection;
foreach (var player in GetOtherPlayers()) EmitSignal(nameof(StatusChanged), Status);
player.RemoveFromParent();
CurrentStatus = Status.NoConnection; ClearPlayers();
EmitSignal(nameof(StatusChanged), CurrentStatus);
} }
public Player FindLocalPlayer()
=> GetNode<Player>("/root/Game/LocalPlayer");
public Node2D GetPlayerWithId(int id) private void OnClientConnected()
=> PlayerContainer.GetNodeOrNull<Node2D>(id.ToString());
public Node2D GetOrCreatePlayerWithId(int id)
{ {
var player = GetPlayerWithId(id); Status = NetworkStatus.Authenticating;
if (player == null) { EmitSignal(nameof(StatusChanged), Status);
player = (Node2D)OtherPlayer.Instance();
// TODO: Use "set_network_master". var id = GetTree().GetNetworkUniqueId();
player.Name = id.ToString(); Game.LocalPlayer.NetworkId = id;
PlayerContainer.AddChild(player); _playersById.Add(id, Game.LocalPlayer);
}
return player; Rpc(nameof(OnClientAuthenticate), Game.LocalPlayer.DisplayName, Game.LocalPlayer.Color);
} }
// TODO: This assumes that any node whose name starts with a digit is a player. [Master]
public IEnumerable<Node2D> GetOtherPlayers() private void OnClientAuthenticate(string displayName, Color color)
=> PlayerContainer.GetChildren().OfType<Node2D>() {
.Where(node => char.IsDigit(node.Name[0])); var id = GetTree().GetRpcSenderId();
// Authentication message is only sent once, so once the Player object exists, ignore this message.
if (GetPlayer(id) != null) return;
var newPlayer = SpawnOtherPlayerInternal(id, Vector2.Zero, displayName, color);
RpcId(id, nameof(SpawnLocalPlayer), newPlayer.Position);
private void OnClientConnected() foreach (var player in _playersById.Values) {
if (player == newPlayer) continue;
// Spawn existing players for the new player.
RpcId(id, nameof(SpawnOtherPlayer), player.NetworkId, player.Position, player.DisplayName, player.Color);
// Spawn new player for existing players.
if (!player.IsLocal) // Don't spawn the player for the host, it already called SpawnOtherPlayer itself.
RpcId(player.NetworkId, nameof(SpawnOtherPlayer), newPlayer.NetworkId, newPlayer.Position, newPlayer.DisplayName, newPlayer.Color);
}
}
[Puppet]
private Player SpawnLocalPlayer(Vector2 position)
{ {
LocalPlayer = FindLocalPlayer(); Status = NetworkStatus.ConnectedToServer;
EmitSignal(nameof(StatusChanged), Status);
CurrentStatus = Status.ConnectedToServer; Game.LocalPlayer.Position = position;
EmitSignal(nameof(StatusChanged), CurrentStatus); return Game.LocalPlayer;
} }
private Player SpawnOtherPlayerInternal(int id, Vector2 position, string displayName, Color color)
{
var player = OtherPlayerScene.Init<Player>();
player.NetworkId = id;
// TODO: We need to find a way to sync these property automatically.
player.Position = position;
player.DisplayName = displayName;
player.Color = color;
_playersById.Add(id, player);
PlayerContainer.AddChild(player);
return player;
}
[Puppet]
private void SpawnOtherPlayer(int id, Vector2 position, string displayName, Color color)
=> SpawnOtherPlayerInternal(id, position, displayName, color);
private void OnPeerConnected(int id) private void OnPeerConnected(int id)
{ {
// Currently unused.
} }
private void OnPeerDisconnected(int id) private void OnPeerDisconnected(int id)
=> GetPlayerWithId(id)?.RemoveFromParent();
[Remote]
private void OnPlayerMoved(Vector2 position)
{ {
var id = GetTree().GetRpcSenderId(); GetPlayer(id)?.RemoveFromParent();
var player = GetOrCreatePlayerWithId(id); _playersById.Remove(id);
player.Position = position;
} }
} }

@ -1,42 +1,93 @@
using Godot; using Godot;
using System;
public class Player : KinematicBody2D // FIXME: Player name should not be stored in "Name".
public class Player : KinematicBody2D, IInitializer
{ {
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F); [Export] public NodePath DisplayNamePath { get; set; }
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F); [Export] public NodePath SpritePath { get; set; }
[Export] public float Speed { get; set; } = 120; public Label DisplayNameLabel { get; private set; }
[Export] public float JumpSpeed { get; set; } = 180; public Sprite Sprite { get; private set; }
[Export] public float Gravity { get; set; } = 400; public Network Network { get; private set; }
[Export(PropertyHint.Range, "0,1")] public bool IsLocal => this is LocalPlayer;
public float Friction { get; set; } = 0.1F;
[Export(PropertyHint.Range, "0,1")]
public float Acceleration { get; set; } = 0.25F;
private Vector2 _velocity = Vector2.Zero; private int _networkId = -1;
private DateTime? _jumpPressed = null; public int NetworkId {
private DateTime? _lastOnFloor = null; get => _networkId;
set => SetNetworkId(value);
}
public Color Color {
get => Sprite.Modulate;
set => SetColor(value);
}
public string DisplayName {
get => DisplayNameLabel.Text;
set => SetDisplayName(value);
}
public void Initialize()
{
DisplayNameLabel = GetNode<Label>(DisplayNamePath);
Sprite = GetNode<Sprite>(SpritePath);
}
public override void _Ready()
{
Initialize();
Network = GetNode<Network>("/root/Game/Network");
RsetConfig("position", MultiplayerAPI.RPCMode.Puppetsync);
Sprite.RsetConfig("modulate", MultiplayerAPI.RPCMode.Puppetsync);
DisplayNameLabel.RsetConfig("text", MultiplayerAPI.RPCMode.Puppetsync);
}
public override void _Process(float delta)
{
if (GetTree().NetworkPeer != null) {
// TODO: Only send position if it changed.
// Send unreliable messages while moving, and a reliable once the player stopped.
if (GetTree().IsNetworkServer())
this.RsetUnreliableExcept(NetworkId, "position", Position);
else if (Network.Status == NetworkStatus.ConnectedToServer)
RpcUnreliable(nameof(OnPositionChanged), Position);
}
}
[Master]
private void OnPositionChanged(Vector2 value)
{ if (GetTree().GetRpcSenderId() == NetworkId) Position = value; }
private void SetNetworkId(int value)
{
_networkId = value;
Name = (_networkId > 0) ? value.ToString() : "LocalPlayer";
}
private void SetColor(Color value)
{
Sprite.Modulate = value;
if (IsInsideTree() && GetTree().NetworkPeer != null) {
if (GetTree().IsNetworkServer()) Sprite.RsetExcept(NetworkId, "modulate", value);
else Rpc(nameof(OnColorChanged), value);
}
}
[Master]
private void OnColorChanged(Color value)
{ if (GetTree().GetRpcSenderId() == NetworkId) Color = value; }
public override void _PhysicsProcess(float delta) private void SetDisplayName(string value)
{ {
var moveDir = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left"); DisplayNameLabel.Text = value;
_velocity.x = (moveDir != 0) ? Mathf.Lerp(_velocity.x, moveDir * Speed, Acceleration) if (IsInsideTree() && GetTree().NetworkPeer != null) {
: Mathf.Lerp(_velocity.x, 0, Friction); if (GetTree().IsNetworkServer()) DisplayNameLabel.RsetExcept(NetworkId, "text", value);
_velocity.y += Gravity * delta; else Rpc(nameof(OnDisplayNameChanged), value);
_velocity = MoveAndSlide(_velocity, Vector2.Up);
if (Input.IsActionJustPressed("move_jump"))
_jumpPressed = DateTime.Now;
if (IsOnFloor())
_lastOnFloor = DateTime.Now;
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) &&
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) {
_velocity.y = -JumpSpeed;
_jumpPressed = null;
_lastOnFloor = null;
} }
} }
[Master]
private void OnDisplayNameChanged(string value)
{ if (GetTree().GetRpcSenderId() == NetworkId) DisplayName = value; }
} }

@ -8,7 +8,7 @@ public class Viewport : Node
public override void _Ready() public override void _Ready()
{ {
GetTree().Connect("screen_resized", this, "OnWindowResized"); GetTree().Connect("screen_resized", this, nameof(OnWindowResized));
OnWindowResized(); OnWindowResized();
} }

Loading…
Cancel
Save