Skip to content

Instantly share code, notes, and snippets.

@define-private-public
Last active October 5, 2022 15:12
Show Gist options
  • Select an option

  • Save define-private-public/faa298981017489829e6127e75503a86 to your computer and use it in GitHub Desktop.

Select an option

Save define-private-public/faa298981017489829e6127e75503a86 to your computer and use it in GitHub Desktop.
The classic game of Pong, that runs over UDP (uses MonoGame/XNA).
// Filename: Arena.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.Collections.Concurrent;
using Microsoft.Xna.Framework;
namespace PongGame
{
public enum ArenaState
{
NotRunning,
WaitingForPlayers,
NotifyingGameStart,
InGame,
GameOver
}
// This is where the game takes place
public class Arena
{
// Game objects & state info
public ThreadSafe<ArenaState> State { get; private set; } = new ThreadSafe<ArenaState>();
private Ball _ball = new Ball();
public PlayerInfo LeftPlayer { get; private set; } = new PlayerInfo(); // contains Paddle
public PlayerInfo RightPlayer { get; private set; } = new PlayerInfo(); // contains Paddle
private object _setPlayerLock = new object();
private Stopwatch _gameTimer = new Stopwatch();
// Connection info
private PongServer _server;
private TimeSpan _heartbeatTimeout = TimeSpan.FromSeconds(20);
// Packet queue
private ConcurrentQueue<NetworkMessage> _messages = new ConcurrentQueue<NetworkMessage>();
// Shutdown data
private ThreadSafe<bool> _stopRequested = new ThreadSafe<bool>(false);
// Other
private Thread _arenaThread;
private Random _random = new Random();
public readonly int Id;
private static int _nextId = 1;
public Arena(PongServer server)
{
_server = server;
Id = _nextId++;
State.Value = ArenaState.NotRunning;
// Some other data
LeftPlayer.Paddle = new Paddle(PaddleSide.Left);
RightPlayer.Paddle = new Paddle(PaddleSide.Right);
}
// Returns true if the player was added,
// false otherwise (max two players will be accepeted)
public bool TryAddPlayer(IPEndPoint playerIP)
{
if (State.Value == ArenaState.WaitingForPlayers)
{
lock (_setPlayerLock)
{
// First do the left
if (!LeftPlayer.IsSet)
{
LeftPlayer.Endpoint = playerIP;
return true;
}
// Then the Right
if (!RightPlayer.IsSet)
{
RightPlayer.Endpoint = playerIP;
return true;
}
}
}
// Couldn't add any more
return false;
}
// Initializes the game objects to a default state and start a new Thread
public void Start()
{
// Shift the state
State.Value = ArenaState.WaitingForPlayers;
// Start the internal thread on Run
_arenaThread = new Thread(new ThreadStart(_arenaRun));
_arenaThread.Start();
}
// Tells the game to stop
public void Stop()
{
_stopRequested.Value = true;
}
// This runs in its own Thread
// It is the actual game
private void _arenaRun()
{
Console.WriteLine("[{0:000}] Waiting for players", Id);
GameTime gameTime = new GameTime();
// Varibables used in the switch
TimeSpan notifyGameStartTimeout = TimeSpan.FromSeconds(2.5);
TimeSpan sendGameStateTimeout = TimeSpan.FromMilliseconds(1000f / 30f); // How often to update the players
// The loop
bool running = true;
bool playerDropped = false;
while (running)
{
// Pop off a message (if there is one)
NetworkMessage message;
bool haveMsg = _messages.TryDequeue(out message);
switch (State.Value)
{
case ArenaState.WaitingForPlayers:
if (haveMsg)
{
// Wait until we have two players
_handleConnectionSetup(LeftPlayer, message);
_handleConnectionSetup(RightPlayer, message);
// Check if we are ready or not
if (LeftPlayer.HavePaddle && RightPlayer.HavePaddle)
{
// Try sending the GameStart packet immediately
_notifyGameStart(LeftPlayer, new TimeSpan());
_notifyGameStart(RightPlayer, new TimeSpan());
// Shift the state
State.Value = ArenaState.NotifyingGameStart;
}
}
break;
case ArenaState.NotifyingGameStart:
// Try sending the GameStart packet
_notifyGameStart(LeftPlayer, notifyGameStartTimeout);
_notifyGameStart(RightPlayer, notifyGameStartTimeout);
// Check for ACK
if (haveMsg && (message.Packet.Type == PacketType.GameStartAck))
{
// Mark true for those who have sent something
if (message.Sender.Equals(LeftPlayer.Endpoint))
LeftPlayer.Ready = true;
else if (message.Sender.Equals(RightPlayer.Endpoint))
RightPlayer.Ready = true;
}
// Are we ready to send/received game data?
if (LeftPlayer.Ready && RightPlayer.Ready)
{
// Initlize some game object positions
_ball.Initialize();
LeftPlayer.Paddle.Initialize();
RightPlayer.Paddle.Initialize();
// Send a basic game state
_sendGameState(LeftPlayer, new TimeSpan());
_sendGameState(RightPlayer, new TimeSpan());
// Start the game timer
State.Value = ArenaState.InGame;
Console.WriteLine("[{0:000}] Starting Game", Id);
_gameTimer.Start();
}
break;
case ArenaState.InGame:
// Update the game timer
TimeSpan now = _gameTimer.Elapsed;
gameTime = new GameTime(now, now - gameTime.TotalGameTime);
// Get paddle postions from clients
if (haveMsg)
{
switch (message.Packet.Type)
{
case PacketType.PaddlePosition:
_handlePaddleUpdate(message);
break;
case PacketType.Heartbeat:
// Respond with an ACK
HeartbeatAckPacket hap = new HeartbeatAckPacket();
PlayerInfo player = message.Sender.Equals(LeftPlayer.Endpoint) ? LeftPlayer : RightPlayer;
_sendTo(player, hap);
// Record time
player.LastPacketReceivedTime = message.ReceiveTime;
break;
}
}
//Update the game components
_ball.ServerSideUpdate(gameTime);
_checkForBallCollisions();
// Send the data
_sendGameState(LeftPlayer, sendGameStateTimeout);
_sendGameState(RightPlayer, sendGameStateTimeout);
break;
}
// Check for a quit from one of the clients
if (haveMsg && (message.Packet.Type == PacketType.Bye))
{
// Well, someone dropped
PlayerInfo player = message.Sender.Equals(LeftPlayer.Endpoint) ? LeftPlayer : RightPlayer;
running = false;
Console.WriteLine("[{0:000}] Quit detected from {1} at {2}",
Id, player.Paddle.Side, _gameTimer.Elapsed);
// Tell the other one
if (player.Paddle.Side == PaddleSide.Left)
{
// Left Quit, tell Right
if (RightPlayer.IsSet)
_server.SendBye(RightPlayer.Endpoint);
}
else
{
// Right Quit, tell Left
if (LeftPlayer.IsSet)
_server.SendBye(LeftPlayer.Endpoint);
}
}
// Check for timeouts
playerDropped |= _timedOut(LeftPlayer);
playerDropped |= _timedOut(RightPlayer);
// Small nap
Thread.Sleep(1);
// Check quit values
running &= !_stopRequested.Value;
running &= !playerDropped;
}
// End the game
_gameTimer.Stop();
State.Value = ArenaState.GameOver;
Console.WriteLine("[{0:000}] Game Over, total game time was {1}", Id, _gameTimer.Elapsed);
// If the stop was requested, gracefully tell the players to quit
if (_stopRequested.Value)
{
Console.WriteLine("[{0:000}] Notifying Players of server shutdown", Id);
if (LeftPlayer.IsSet)
_server.SendBye(LeftPlayer.Endpoint);
if (RightPlayer.IsSet)
_server.SendBye(RightPlayer.Endpoint);
}
// Tell the server that we're finished
_server.NotifyDone(this);
}
// Gives the underlying thread 1/10 a second to finish
public void JoinThread()
{
_arenaThread.Join(100);
}
// This is called by the server to dispatch messages to this Arena
public void EnqueMessage(NetworkMessage nm)
{
_messages.Enqueue(nm);
}
#region Network Functions
// Sends a packet to a player and marks other info
private void _sendTo(PlayerInfo player, Packet packet)
{
_server.SendPacket(packet, player.Endpoint);
player.LastPacketSentTime = DateTime.Now;
}
// Returns true if a player has timed out or not
// If we haven't recieved a heartbeat at all from them, they're not timed out
private bool _timedOut(PlayerInfo player)
{
// We haven't recorded it yet
if (player.LastPacketReceivedTime == DateTime.MinValue)
return false;
// Do math
bool timeoutDetected = (DateTime.Now > (player.LastPacketReceivedTime.Add(_heartbeatTimeout)));
if (timeoutDetected)
Console.WriteLine("[{0:000}] Timeout detected on {1} Player at {2}", Id, player.Paddle.Side, _gameTimer.Elapsed);
return timeoutDetected;
}
// This will Handle the initial connection setup and Heartbeats of a client
private void _handleConnectionSetup(PlayerInfo player, NetworkMessage message)
{
// Make sure the message is from the correct client provided
bool sentByPlayer = message.Sender.Equals(player.Endpoint);
if (sentByPlayer)
{
// Record the last time we've heard from them
player.LastPacketReceivedTime = message.ReceiveTime;
// Do they need their Side? or a heartbeat ACK
switch (message.Packet.Type)
{
case PacketType.RequestJoin:
Console.WriteLine("[{0:000}] Join Request from {1}", Id, player.Endpoint);
_sendAcceptJoin(player);
break;
case PacketType.AcceptJoinAck:
// They acknowledged (they will send heartbeats until game start)
player.HavePaddle = true;
break;
case PacketType.Heartbeat:
// They are waiting for the game start, Respond with an ACK
HeartbeatAckPacket hap = new HeartbeatAckPacket();
_sendTo(player, hap);
// Incase their ACK didn't reach us
if (!player.HavePaddle)
_sendAcceptJoin(player);
break;
}
}
}
// Sends an AcceptJoinPacket to a player
public void _sendAcceptJoin(PlayerInfo player)
{
// They need to know which paddle they are
AcceptJoinPacket ajp = new AcceptJoinPacket();
ajp.Side = player.Paddle.Side;
_sendTo(player, ajp);
}
// Tries to notify the players of a GameStart
// retryTimeout is how long to wait until to resending the packet
private void _notifyGameStart(PlayerInfo player, TimeSpan retryTimeout)
{
// check if they are ready already
if (player.Ready)
return;
// Make sure not to spam them
if (DateTime.Now >= (player.LastPacketSentTime.Add(retryTimeout)))
{
GameStartPacket gsp = new GameStartPacket();
_sendTo(player, gsp);
}
}
// Sends information about the current game state to the players
// resendTimeout is how long to wait until sending another GameStatePacket
private void _sendGameState(PlayerInfo player, TimeSpan resendTimeout)
{
if (DateTime.Now >= (player.LastPacketSentTime.Add(resendTimeout)))
{
// Set the data
GameStatePacket gsp = new GameStatePacket();
gsp.LeftY = LeftPlayer.Paddle.Position.Y;
gsp.RightY = RightPlayer.Paddle.Position.Y;
gsp.BallPosition = _ball.Position;
gsp.LeftScore = LeftPlayer.Paddle.Score;
gsp.RightScore = RightPlayer.Paddle.Score;
_sendTo(player, gsp);
}
}
// Tells both of the clients to play a sound effect
private void _playSoundEffect(string sfxName)
{
// Make the packet
PlaySoundEffectPacket packet = new PlaySoundEffectPacket();
packet.SFXName = sfxName;
_sendTo(LeftPlayer, packet);
_sendTo(RightPlayer, packet);
}
// This updates a paddle's postion from a client
// `message.Packet.Type` must be `PacketType.PaddlePosition`
// TODO add some "cheat detection,"
private void _handlePaddleUpdate(NetworkMessage message)
{
// Only two possible players
PlayerInfo player = message.Sender.Equals(LeftPlayer.Endpoint) ? LeftPlayer : RightPlayer;
// Make sure we use the latest message **SENT BY THE CLIENT** ignore it otherwise
if (message.Packet.Timestamp > player.LastPacketReceivedTimestamp)
{
// record timestamp and time
player.LastPacketReceivedTimestamp = message.Packet.Timestamp;
player.LastPacketReceivedTime = message.ReceiveTime;
// "cast" the packet and set data
PaddlePositionPacket ppp = new PaddlePositionPacket(message.Packet.GetBytes());
player.Paddle.Position.Y = ppp.Y;
}
}
#endregion // Network Functions
#region Collision Methods
// Does all of the collision logic for the ball (including scores)
private void _checkForBallCollisions()
{
// Top/Bottom
float ballY = _ball.Position.Y;
if ((ballY <= _ball.TopmostY) || (ballY >= _ball.BottommostY))
{
_ball.Speed.Y *= -1;
_playSoundEffect("ball-hit");
}
// Ball left and right (the goals!)
float ballX = _ball.Position.X;
if (ballX <= _ball.LeftmostX)
{
// Right player scores! (reset ball)
RightPlayer.Paddle.Score += 1;
Console.WriteLine("[{0:000}] Right Player scored ({1} -- {2}) at {3}",
Id, LeftPlayer.Paddle.Score, RightPlayer.Paddle.Score, _gameTimer.Elapsed);
_ball.Initialize();
_playSoundEffect("score");
}
else if (ballX >= _ball.RightmostX)
{
// Left palyer scores! (reset ball)
LeftPlayer.Paddle.Score += 1;
Console.WriteLine("[{0:000}] Left Player scored ({1} -- {2}) at {3}",
Id, LeftPlayer.Paddle.Score, RightPlayer.Paddle.Score, _gameTimer.Elapsed);
_ball.Initialize();
_playSoundEffect("score");
}
// Ball with paddles
PaddleCollision collision;
if (LeftPlayer.Paddle.Collides(_ball, out collision))
_processBallHitWithPaddle(collision);
if (RightPlayer.Paddle.Collides(_ball, out collision))
_processBallHitWithPaddle(collision);
}
// Modifies the ball state based on what the collision is
private void _processBallHitWithPaddle(PaddleCollision collision)
{
// Safety check
if (collision == PaddleCollision.None)
return;
// Increase the speed
_ball.Speed.X *= _map((float)_random.NextDouble(), 0, 1, 1, 1.25f);
_ball.Speed.Y *= _map((float)_random.NextDouble(), 0, 1, 1, 1.25f);
// Shoot in the opposite direction
_ball.Speed.X *= -1;
// Hit with top or bottom?
if ((collision == PaddleCollision.WithTop) || (collision == PaddleCollision.WithBottom))
_ball.Speed.Y *= -1;
// Play a sound on the client
_playSoundEffect("ballHit");
}
#endregion // Collision Methods
// Small utility function that maps one value range to another
private float _map(float x, float a, float b, float p, float q)
{
return p + (x - a) * (q - p) / (b - a);
}
}
}
// Filename: Ball.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
namespace PongGame
{
// The ball that's bounced around
public class Ball
{
// Statics
public static Vector2 InitialSpeed = new Vector2(60f, 60f);
// Private data members
private Texture2D _sprite;
private Random _random = new Random(); // Random Number Generator
// Public data members
public Vector2 Position = new Vector2();
public Vector2 Speed;
public int LeftmostX { get; private set; } // Bounds
public int RightmostX { get; private set; }
public int TopmostY { get; private set; }
public int BottommostY { get; private set; }
// What gets hit
public Rectangle CollisionArea
{
get { return new Rectangle(Position.ToPoint(), GameGeometry.BallSize); }
}
public void LoadContent(ContentManager content)
{
_sprite = content.Load<Texture2D>("ball.png");
}
// this is used to reset the postion of the ball to the center of the board
public void Initialize()
{
// Center the ball
Rectangle playAreaRect = new Rectangle(new Point(0, 0), GameGeometry.PlayArea);
Position = playAreaRect.Center.ToVector2();
Position = Vector2.Subtract(Position, GameGeometry.BallSize.ToVector2() / 2f);
// Set the velocity
Speed = InitialSpeed;
// Randomize direction
if (_random.Next() % 2 == 1)
Speed.X *= -1;
if (_random.Next() % 2 == 1)
Speed.Y *= -1;
// Set bounds
LeftmostX = 0;
RightmostX = playAreaRect.Width - GameGeometry.BallSize.X;
TopmostY = 0;
BottommostY = playAreaRect.Height - GameGeometry.BallSize.Y;
}
// Moves the ball, should only be called on the server
public void ServerSideUpdate(GameTime gameTime)
{
float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
// Add the distance
Position = Vector2.Add(Position, timeDelta * Speed);
}
// Draws the ball to the screen, only called on the client
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
spriteBatch.Draw(_sprite, Position);
}
}
}
// Filename: GameGeometry.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using Microsoft.Xna.Framework;
namespace PongGame
{
// This is a class that contains all the information for the geometry
// of the objects in the game (play area, paddle/ball sizes, etc.)
public static class GameGeometry
{
public static readonly Point PlayArea = new Point(320, 240); // Client area
public static readonly Vector2 ScreenCenter // Center point of the screen
= new Vector2(PlayArea.X / 2f, PlayArea.Y / 2f);
public static readonly Point BallSize = new Point(8, 8); // Size of Ball
public static readonly Point PaddleSize = new Point(8, 44); // Size of the Paddles
public static readonly int GoalSize = 12; // Width behind paddle
public static readonly float PaddleSpeed = 100f; // Speed of the paddle, (pixels/sec)
}
}
// Filename: NetworkMessage.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using System.Net;
namespace PongGame
{
// Data structure used to store Packets along with their sender
public class NetworkMessage
{
public IPEndPoint Sender { get; set; }
public Packet Packet { get; set; }
public DateTime ReceiveTime { get; set; }
}
}
// Filename: Packet.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using System.Text;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using Microsoft.Xna.Framework;
using System.Runtime.InteropServices;
namespace PongGame
{
// These are used to define what each packet means
// they all have different payloads (if any)
// Check for subclasses below the `Packet` class
public enum PacketType : uint
{
RequestJoin = 1, // Client Request to join a game
AcceptJoin, // Server accepts join
AcceptJoinAck, // Client acknowledges the AcceptJoin
Heartbeat, // Client tells Server its alive (before GameStart)
HeartbeatAck, // Server acknowledges Client's Heartbeat (before GameStart)
GameStart, // Server tells Clients game is starting
GameStartAck, // Client acknowledges the GameStart
PaddlePosition, // Client tell Server position of the their paddle
GameState, // Server tells Clients ball & paddle position, and scores
PlaySoundEffect, // Server tells the clinet to play a sound effect
Bye, // Either Server or Client tells the other to end the connection
}
public class Packet
{
// Packet Data
public PacketType Type;
public long Timestamp; // 64 bit timestamp from DateTime.Ticks
public byte[] Payload = new byte[0];
#region Constructors
// Creates a Packet with the set type and an empty Payload
public Packet(PacketType type)
{
this.Type = type;
Timestamp = DateTime.Now.Ticks;
}
// Creates a Packet from a byte array
public Packet(byte[] bytes)
{
// Start peeling out the data from the byte array
int i = 0;
// Type
this.Type = (PacketType)BitConverter.ToUInt32(bytes, 0);
i += sizeof(PacketType);
// Timestamp
Timestamp = BitConverter.ToInt64(bytes, i);
i += sizeof(long);
// Rest is payload
Payload = bytes.Skip(i).ToArray();
}
#endregion // Constructors
// Gets the packet as a byte array
public byte[] GetBytes()
{
int ptSize = sizeof(PacketType);
int tsSize = sizeof(long);
// Join the Packet data
int i = 0;
byte[] bytes = new byte[ptSize + tsSize + Payload.Length];
// Type
BitConverter.GetBytes((uint)this.Type).CopyTo(bytes, i);
i += ptSize;
// Timestamp
BitConverter.GetBytes(Timestamp).CopyTo(bytes, i);
i += tsSize;
// Payload
Payload.CopyTo(bytes, i);
i += Payload.Length;
return bytes;
}
public override string ToString()
{
return string.Format("[Packet:{0}\n timestamp={1}\n payload size={2}]",
this.Type, new DateTime(Timestamp), Payload.Length);
}
// Sends a Packet to a specific receiver
public void Send(UdpClient client, IPEndPoint receiver)
{
// TODO maybe be async instead?
byte[] bytes = GetBytes();
client.Send(bytes, bytes.Length, receiver);
}
// Send a Packet to the default remote receiver (will throw error if not set)
public void Send(UdpClient client)
{
byte[] bytes = GetBytes();
client.Send(bytes, bytes.Length);
}
}
#region Specific Packets
// Client Join Request
public class RequestJoinPacket : Packet
{
public RequestJoinPacket()
: base(PacketType.RequestJoin)
{
}
}
// Server Accept Request Join, assigns a paddle
public class AcceptJoinPacket : Packet
{
// Paddle side
public PaddleSide Side {
get { return (PaddleSide)BitConverter.ToUInt32(Payload, 0); }
set { Payload = BitConverter.GetBytes((uint)value); }
}
public AcceptJoinPacket()
: base(PacketType.AcceptJoin)
{
Payload = new byte[sizeof(PaddleSide)];
// Set a dfeault paddle of None
Side = PaddleSide.None;
}
public AcceptJoinPacket(byte[] bytes)
: base(bytes)
{
}
}
// Ack packet for the one above
public class AcceptJoinAckPacket : Packet
{
public AcceptJoinAckPacket()
: base(PacketType.AcceptJoinAck)
{
}
}
// Client tells the Server it's alive
public class HeartbeatPacket : Packet
{
public HeartbeatPacket()
: base(PacketType.Heartbeat)
{
}
}
// Server tells the client is knows it's alive
public class HeartbeatAckPacket : Packet
{
public HeartbeatAckPacket()
: base(PacketType.HeartbeatAck)
{
}
}
// Tells the client to begin sending data
public class GameStartPacket : Packet
{
public GameStartPacket()
: base(PacketType.GameStart)
{
}
}
// Ack for the packet above
public class GameStartAckPacket : Packet
{
public GameStartAckPacket()
: base(PacketType.GameStartAck)
{
}
}
// Sent by the client to tell the server it's Y Position for the Paddle
public class PaddlePositionPacket : Packet
{
// The Paddle's Y position
public float Y {
get { return BitConverter.ToSingle(Payload, 0); }
set { BitConverter.GetBytes(value).CopyTo(Payload, 0); }
}
public PaddlePositionPacket()
: base(PacketType.PaddlePosition)
{
Payload = new byte[sizeof(float)];
// Default value is zero
Y = 0;
}
public PaddlePositionPacket(byte[] bytes)
: base(bytes)
{
}
public override string ToString()
{
return string.Format("[Packet:{0}\n timestamp={1}\n payload size={2}" +
"\n Y={3}]",
this.Type, new DateTime(Timestamp), Payload.Length, Y);
}
}
// Sent by the server to thd Clients to update the game information
public class GameStatePacket : Packet
{
// Payload array offets
private static readonly int _leftYIndex = 0;
private static readonly int _rightYIndex = 4;
private static readonly int _ballPositionIndex = 8;
private static readonly int _leftScoreIndex = 16;
private static readonly int _rightScoreIndex = 20;
// The Left Paddle's Y position
public float LeftY
{
get { return BitConverter.ToSingle(Payload, _leftYIndex); }
set { BitConverter.GetBytes(value).CopyTo(Payload, _leftYIndex); }
}
// Right Paddle's Y Position
public float RightY
{
get { return BitConverter.ToSingle(Payload, _rightYIndex); }
set { BitConverter.GetBytes(value).CopyTo(Payload, _rightYIndex); }
}
// Ball position
public Vector2 BallPosition
{
get {
return new Vector2(
BitConverter.ToSingle(Payload, _ballPositionIndex),
BitConverter.ToSingle(Payload, _ballPositionIndex + sizeof(float))
);
}
set {
BitConverter.GetBytes(value.X).CopyTo(Payload, _ballPositionIndex);
BitConverter.GetBytes(value.Y).CopyTo(Payload, _ballPositionIndex + sizeof(float));
}
}
// Left Paddle's Score
public int LeftScore
{
get { return BitConverter.ToInt32(Payload, _leftScoreIndex); }
set { BitConverter.GetBytes(value).CopyTo(Payload, _leftScoreIndex); }
}
// Right Paddle's Score
public int RightScore
{
get { return BitConverter.ToInt32(Payload, _rightScoreIndex); }
set { BitConverter.GetBytes(value).CopyTo(Payload, _rightScoreIndex); }
}
public GameStatePacket()
: base(PacketType.GameState)
{
// Allocate data for the payload (we really shouldn't hardcode this in...)
Payload = new byte[24];
// Set default data
LeftY = 0;
RightY = 0;
BallPosition = new Vector2();
LeftScore = 0;
RightScore = 0;
}
public GameStatePacket(byte[] bytes)
: base(bytes)
{
}
public override string ToString()
{
return string.Format(
"[Packet:{0}\n timestamp={1}\n payload size={2}" +
"\n LeftY={3}" +
"\n RightY={4}" +
"\n BallPosition={5}" +
"\n LeftScore={6}" +
"\n RightScore={7}]",
this.Type, new DateTime(Timestamp), Payload.Length, LeftY, RightY, BallPosition, LeftScore, RightScore);
}
}
// Sent by the Server to tell the client they should play a sound effect
public class PlaySoundEffectPacket : Packet
{
public string SFXName {
get { return Encoding.UTF8.GetString(Payload); }
set { Payload = Encoding.UTF8.GetBytes(value); }
}
public PlaySoundEffectPacket()
: base(PacketType.PlaySoundEffect)
{
SFXName = "";
}
public PlaySoundEffectPacket(byte[] bytes)
: base(bytes)
{
}
public override string ToString()
{
return string.Format(
"[Packet:{0}\n timestamp={1}\n payload size={2}" +
"\n SFXName={3}",
this.Type, new DateTime(Timestamp), Payload.Length, SFXName);
}
}
// Sent by either the Client or the Server to end the game/connection
public class ByePacket : Packet
{
public ByePacket()
: base(PacketType.Bye)
{
}
}
#endregion // Specific Packets
}
// Filename: Paddle.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
namespace PongGame
{
public enum PaddleSide : uint
{
None,
Left,
Right
};
// Type of collision with paddle
public enum PaddleCollision
{
None,
WithTop,
WithFront,
WithBottom
};
// This is the Paddle class for the Server
public class Paddle
{
// Private data members
private Texture2D _sprite;
private DateTime _lastCollisiontime = DateTime.MinValue;
private TimeSpan _minCollisionTimeGap = TimeSpan.FromSeconds(0.2);
// Public data members
public readonly PaddleSide Side;
public int Score = 0;
public Vector2 Position = new Vector2();
public int TopmostY { get; private set; } // Bounds
public int BottommostY { get; private set; }
#region Collision objects
public Rectangle TopCollisionArea
{
get { return new Rectangle(Position.ToPoint(), new Point(GameGeometry.PaddleSize.X, 4)); }
}
public Rectangle BottomCollisionArea
{
get
{
return new Rectangle(
(int)Position.X, FrontCollisionArea.Bottom,
GameGeometry.PaddleSize.X, 4
);
}
}
public Rectangle FrontCollisionArea
{
get
{
Point pos = Position.ToPoint();
pos.Y += 4;
Point size = new Point(GameGeometry.PaddleSize.X, GameGeometry.PaddleSize.Y - 8);
return new Rectangle(pos, size);
}
}
#endregion // Collision objects
// Sets which side the paddle is
public Paddle(PaddleSide side)
{
Side = side;
}
public void LoadContent(ContentManager content)
{
_sprite = content.Load<Texture2D>("paddle.png");
}
// Puts the paddle in the middle of where it can move
public void Initialize()
{
// Figure out where to place the paddle
int x;
if (Side == PaddleSide.Left)
x = GameGeometry.GoalSize;
else if (Side == PaddleSide.Right)
x = GameGeometry.PlayArea.X - GameGeometry.PaddleSize.X - GameGeometry.GoalSize;
else
throw new Exception("Side is not `Left` or `Right`");
Position = new Vector2(x, (GameGeometry.PlayArea.Y / 2) - (GameGeometry.PaddleSize.Y / 2));
Score = 0;
// Set bounds
TopmostY = 0;
BottommostY = GameGeometry.PlayArea.Y - GameGeometry.PaddleSize.Y;
}
// Moves the paddle based on user input (called on Client)
public void ClientSideUpdate(GameTime gameTime)
{
float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
float dist = timeDelta * GameGeometry.PaddleSpeed;
// Check Up & Down keys
KeyboardState kbs = Keyboard.GetState();
if (kbs.IsKeyDown(Keys.Up))
Position.Y -= dist;
else if (kbs.IsKeyDown(Keys.Down))
Position.Y += dist;
// bounds checking
if (Position.Y < TopmostY)
Position.Y = TopmostY;
else if (Position.Y > BottommostY)
Position.Y = BottommostY;
}
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
spriteBatch.Draw(_sprite, Position);
}
// Sees what part of the Paddle collises with the ball (if it does)
public bool Collides(Ball ball, out PaddleCollision typeOfCollision)
{
typeOfCollision = PaddleCollision.None;
// Make sure enough time has passed for a new collisions
// (this prevents a bug where a user can build up a lot of speed in the ball)
if (DateTime.Now < (_lastCollisiontime.Add(_minCollisionTimeGap)))
return false;
// Top & bottom get first priority
if (ball.CollisionArea.Intersects(TopCollisionArea))
{
typeOfCollision = PaddleCollision.WithTop;
_lastCollisiontime = DateTime.Now;
return true;
}
if (ball.CollisionArea.Intersects(BottomCollisionArea))
{
typeOfCollision = PaddleCollision.WithBottom;
_lastCollisiontime = DateTime.Now;
return true;
}
// And check the front
if (ball.CollisionArea.Intersects(FrontCollisionArea))
{
typeOfCollision = PaddleCollision.WithFront;
_lastCollisiontime = DateTime.Now;
return true;
}
// Nope, nothing
return false;
}
}
}
// Filename: PlayerInfo.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using System.Net;
namespace PongGame
{
// Data structure for the server to manage clients
public class PlayerInfo
{
public Paddle Paddle;
public IPEndPoint Endpoint;
public DateTime LastPacketReceivedTime = DateTime.MinValue; // From Server Time
public DateTime LastPacketSentTime = DateTime.MinValue; // From Server Time
public long LastPacketReceivedTimestamp = 0; // From Client Time
public bool HavePaddle = false;
public bool Ready = false;
public bool IsSet {
get { return Endpoint != null; }
}
}
}
// Filename: PongClient.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
// Uncomment the line below to play sounds
// This is here because MonoGame 3.5 on Linux throws an error when trying to play a SoundEffect
//#define CAN_PLAY_SOUNDS
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Collections.Concurrent;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
namespace PongGame
{
public enum ClientState
{
NotConnected,
EstablishingConnection,
WaitingForGameStart,
InGame,
GameOver,
}
class PongClient : Game
{
// Game stuffs
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
// Network stuff
private UdpClient _udpClient;
public readonly string ServerHostname;
public readonly int ServerPort;
// Time measurement
private DateTime _lastPacketReceivedTime = DateTime.MinValue; // From Client Time
private DateTime _lastPacketSentTime = DateTime.MinValue; // From Client Time
private long _lastPacketReceivedTimestamp = 0; // From Server Time
private TimeSpan _heartbeatTimeout = TimeSpan.FromSeconds(20);
private TimeSpan _sendPaddlePositionTimeout = TimeSpan.FromMilliseconds(1000f / 30f); // How often to update the server
// Messaging
private Thread _networkThread;
private ConcurrentQueue<NetworkMessage> _incomingMessages
= new ConcurrentQueue<NetworkMessage>();
private ConcurrentQueue<Packet> _outgoingMessages
= new ConcurrentQueue<Packet>();
// Game objects
private Ball _ball;
private Paddle _left;
private Paddle _right;
private Paddle _ourPaddle;
private float _previousY;
// Info messages for the user
private Texture2D _establishingConnectionMsg;
private Texture2D _waitingForGameStartMsg;
private Texture2D _gamveOverMsg;
// Audio
private SoundEffect _ballHitSFX;
private SoundEffect _scoreSFX;
// State stuff
private ClientState _state = ClientState.NotConnected;
private ThreadSafe<bool> _running = new ThreadSafe<bool>(false);
private ThreadSafe<bool> _sendBye = new ThreadSafe<bool>(false);
public PongClient(string hostname, int port)
{
// Content
Content.RootDirectory = "Content";
// Graphics setup
_graphics = new GraphicsDeviceManager(this);
_graphics.PreferredBackBufferWidth = GameGeometry.PlayArea.X;
_graphics.PreferredBackBufferHeight = GameGeometry.PlayArea.Y;
_graphics.IsFullScreen = false;
_graphics.ApplyChanges();
// Game Objects
_ball = new Ball();
_left = new Paddle(PaddleSide.Left);
_right = new Paddle(PaddleSide.Right);
// Connection stuff
ServerHostname = hostname;
ServerPort = port;
_udpClient = new UdpClient(ServerHostname, ServerPort);
}
protected override void Initialize()
{
base.Initialize();
_left.Initialize();
_right.Initialize();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(_graphics.GraphicsDevice);
// Load the game objects
_ball.LoadContent(Content);
_left.LoadContent(Content);
_right.LoadContent(Content);
// Load the messages
_establishingConnectionMsg = Content.Load<Texture2D>("establishing-connection-msg.png");
_waitingForGameStartMsg = Content.Load<Texture2D>("waiting-for-game-start-msg.png");
_gamveOverMsg = Content.Load<Texture2D>("game-over-msg.png");
// Load sound effects
_ballHitSFX = Content.Load<SoundEffect>("ball-hit.wav");
_scoreSFX = Content.Load<SoundEffect>("score.wav");
}
protected override void UnloadContent()
{
// Cleanup
_networkThread?.Join(TimeSpan.FromSeconds(2));
_udpClient.Close();
base.UnloadContent();
}
protected override void Update(GameTime gameTime)
{
// Check for close
KeyboardState kbs = Keyboard.GetState();
if (kbs.IsKeyDown(Keys.Escape))
{
// Player wants to quit, send a ByePacket (if we're connected)
if ((_state == ClientState.EstablishingConnection) ||
(_state == ClientState.WaitingForGameStart) ||
(_state == ClientState.InGame))
{
// Will trigger the network thread to send the Bye Packet
_sendBye.Value = true;
}
// Will stop the network thread
_running.Value = false;
_state = ClientState.GameOver;
Exit();
}
// Check for time out with the server
if (_timedOut())
_state = ClientState.GameOver;
// Get message
NetworkMessage message;
bool haveMsg = _incomingMessages.TryDequeue(out message);
// Check for Bye From server
if (haveMsg && (message.Packet.Type == PacketType.Bye))
{
// Shutdown the network thread (not needed anymore)
_running.Value = false;
_state = ClientState.GameOver;
}
switch (_state)
{
case ClientState.EstablishingConnection:
_sendRequestJoin(TimeSpan.FromSeconds(1));
if (haveMsg)
_handleConnectionSetupResponse(message.Packet);
break;
case ClientState.WaitingForGameStart:
// Send a heartbeat
_sendHeartbeat(TimeSpan.FromSeconds(0.2));
if (haveMsg)
{
switch (message.Packet.Type)
{
case PacketType.AcceptJoin:
// It's possible that they didn't receive our ACK in the previous state
_sendAcceptJoinAck();
break;
case PacketType.HeartbeatAck:
// Record ACK times
_lastPacketReceivedTime = message.ReceiveTime;
if (message.Packet.Timestamp > _lastPacketReceivedTimestamp)
_lastPacketReceivedTimestamp = message.Packet.Timestamp;
break;
case PacketType.GameStart:
// Start the game and ACK it
_sendGameStartAck();
_state = ClientState.InGame;
break;
}
}
break;
case ClientState.InGame:
// Send a heartbeat
_sendHeartbeat(TimeSpan.FromSeconds(0.2));
// update our paddle
_previousY = _ourPaddle.Position.Y;
_ourPaddle.ClientSideUpdate(gameTime);
_sendPaddlePosition(_sendPaddlePositionTimeout);
if (haveMsg)
{
switch (message.Packet.Type)
{
case PacketType.GameStart:
// It's possible the server didn't receive our ACK in the previous state
_sendGameStartAck();
break;
case PacketType.HeartbeatAck:
// Record ACK times
_lastPacketReceivedTime = message.ReceiveTime;
if (message.Packet.Timestamp > _lastPacketReceivedTimestamp)
_lastPacketReceivedTimestamp = message.Packet.Timestamp;
break;
case PacketType.GameState:
// Update the gamestate, make sure its the latest
if (message.Packet.Timestamp > _lastPacketReceivedTimestamp)
{
_lastPacketReceivedTimestamp = message.Packet.Timestamp;
GameStatePacket gsp = new GameStatePacket(message.Packet.GetBytes());
_left.Score = gsp.LeftScore;
_right.Score = gsp.RightScore;
_ball.Position = gsp.BallPosition;
// Update what's not our paddle
if (_ourPaddle.Side == PaddleSide.Left)
_right.Position.Y = gsp.RightY;
else
_left.Position.Y = gsp.LeftY;
}
break;
case PacketType.PlaySoundEffect:
#if CAN_PLAY_SOUNDS
// Play a sound
PlaySoundEffectPacket psep = new PlaySoundEffectPacket(message.Packet.GetBytes());
if (psep.SFXName == "ball-hit")
_ballHitSFX.Play();
else if (psep.SFXName == "score")
_scoreSFX.Play();
#endif
break;
}
}
break;
case ClientState.GameOver:
// Purgatory is here
break;
}
base.Update(gameTime);
}
public void Start()
{
_running.Value = true;
_state = ClientState.EstablishingConnection;
// Start the packet receiving/sending Thread
_networkThread = new Thread(new ThreadStart(_networkRun));
_networkThread.Start();
}
#region Graphical Functions
protected override void Draw(GameTime gameTime)
{
_graphics.GraphicsDevice.Clear(Color.Black);
_spriteBatch.Begin();
// Draw different things based on the state
switch (_state)
{
case ClientState.EstablishingConnection:
_drawCentered(_establishingConnectionMsg);
Window.Title = String.Format("Pong -- Connecting to {0}:{1}", ServerHostname, ServerPort);
break;
case ClientState.WaitingForGameStart:
_drawCentered(_waitingForGameStartMsg);
Window.Title = String.Format("Pong -- Waiting for 2nd Player");
break;
case ClientState.InGame:
// Draw game objects
_ball.Draw(gameTime, _spriteBatch);
_left.Draw(gameTime, _spriteBatch);
_right.Draw(gameTime, _spriteBatch);
// Change the window title
_updateWindowTitleWithScore();
break;
case ClientState.GameOver:
_drawCentered(_gamveOverMsg);
_updateWindowTitleWithScore();
break;
}
_spriteBatch.End();
base.Draw(gameTime);
}
private void _drawCentered(Texture2D texture)
{
Vector2 textureCenter = new Vector2(texture.Width / 2, texture.Height / 2);
_spriteBatch.Draw(texture, GameGeometry.ScreenCenter, null, null, textureCenter);
}
private void _updateWindowTitleWithScore()
{
string fmt = (_ourPaddle.Side == PaddleSide.Left) ?
"[{0}] -- Pong -- {1}" : "{0} -- Pong -- [{1}]";
Window.Title = String.Format(fmt, _left.Score, _right.Score);
}
#endregion // Graphical Functions
#region Network Functions
// This function is meant to be run in its own thread
// and will populate the _incomingMessages queue
private void _networkRun()
{
while (_running.Value)
{
bool canRead = _udpClient.Available > 0;
int numToWrite = _outgoingMessages.Count;
// Get data if there is some
if (canRead)
{
// Read in one datagram
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] data = _udpClient.Receive(ref ep); // Blocks
// Enque a new message
NetworkMessage nm = new NetworkMessage();
nm.Sender = ep;
nm.Packet = new Packet(data);
nm.ReceiveTime = DateTime.Now;
_incomingMessages.Enqueue(nm);
//Console.WriteLine("RCVD: {0}", nm.Packet);
}
// Write out queued
for (int i = 0; i < numToWrite; i++)
{
// Send some data
Packet packet;
bool have = _outgoingMessages.TryDequeue(out packet);
if (have)
packet.Send(_udpClient);
//Console.WriteLine("SENT: {0}", packet);
}
// If Nothing happened, take a nap
if (!canRead && (numToWrite == 0))
Thread.Sleep(1);
}
// Check to see if a bye was requested, one last operation
if (_sendBye.Value)
{
ByePacket bp = new ByePacket();
bp.Send(_udpClient);
Thread.Sleep(1000); // Needs some time to send through
}
}
// Queues up to send a single Packet to the server
private void _sendPacket(Packet packet)
{
_outgoingMessages.Enqueue(packet);
_lastPacketSentTime = DateTime.Now;
}
// Sends a RequestJoinPacket,
private void _sendRequestJoin(TimeSpan retryTimeout)
{
// Make sure not to spam them
if (DateTime.Now >= (_lastPacketSentTime.Add(retryTimeout)))
{
RequestJoinPacket gsp = new RequestJoinPacket();
_sendPacket(gsp);
}
}
// Acks the AcceptJoinPacket
private void _sendAcceptJoinAck()
{
AcceptJoinAckPacket ajap = new AcceptJoinAckPacket();
_sendPacket(ajap);
}
// Responds to the Packets where we are establishing our connection with the server
private void _handleConnectionSetupResponse(Packet packet)
{
// Check for accept and ACK
if (packet.Type == PacketType.AcceptJoin)
{
// Make sure we haven't gotten it before
if (_ourPaddle == null)
{
// See which paddle we are
AcceptJoinPacket ajp = new AcceptJoinPacket(packet.GetBytes());
if (ajp.Side == PaddleSide.Left)
_ourPaddle = _left;
else if (ajp.Side == PaddleSide.Right)
_ourPaddle = _right;
else
throw new Exception("Error, invalid paddle side given by server."); // Should never hit this, but just incase
}
// Send a response
_sendAcceptJoinAck();
// Move the state
_state = ClientState.WaitingForGameStart;
}
}
// Sends a HearbeatPacket to the server
private void _sendHeartbeat(TimeSpan resendTimeout)
{
// Make sure not to spam them
if (DateTime.Now >= (_lastPacketSentTime.Add(resendTimeout)))
{
HeartbeatPacket hp = new HeartbeatPacket();
_sendPacket(hp);
}
}
// Acks the GameStartPacket
private void _sendGameStartAck()
{
GameStartAckPacket gsap = new GameStartAckPacket();
_sendPacket(gsap);
}
// Sends the server our current paddle's Y Position (if it's changed)
private void _sendPaddlePosition(TimeSpan resendTimeout)
{
// Don't send anything if there hasn't been an update
if (_previousY == _ourPaddle.Position.Y)
return;
// Make sure not to spam them
if (DateTime.Now >= (_lastPacketSentTime.Add(resendTimeout)))
{
PaddlePositionPacket ppp = new PaddlePositionPacket();
ppp.Y = _ourPaddle.Position.Y;
_sendPacket(ppp);
}
}
// Returns true if out connection to the server has timed out or not
// If we haven't recieved a packet at all from them, they're not timed out
private bool _timedOut()
{
// We haven't recorded it yet
if (_lastPacketReceivedTime == DateTime.MinValue)
return false;
// Do math
return (DateTime.Now > (_lastPacketReceivedTime.Add(_heartbeatTimeout)));
}
#endregion // Network Functions
#region Program Execution
public static void Main(string[] args)
{
// Get arguements
string hostname = "localhost";//args[0].Trim();
int port = 6000;//int.Parse(args[1].Trim());
// Start the client
PongClient client = new PongClient(hostname, port);
client.Start();
client.Run();
}
#endregion // Program Execution
}
}
// Filename: PongServer.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace PongGame
{
public class PongServer
{
// Network Stuff
private UdpClient _udpClient;
public readonly int Port;
// Messaging
Thread _networkThread;
private ConcurrentQueue<NetworkMessage> _incomingMessages
= new ConcurrentQueue<NetworkMessage>();
private ConcurrentQueue<Tuple<Packet, IPEndPoint>> _outgoingMessages
= new ConcurrentQueue<Tuple<Packet, IPEndPoint>>();
private ConcurrentQueue<IPEndPoint> _sendByePacketTo
= new ConcurrentQueue<IPEndPoint>();
// Arena management
private ConcurrentDictionary<Arena, byte> _activeArenas // Being used as a HashSet
= new ConcurrentDictionary<Arena, byte>();
private ConcurrentDictionary<IPEndPoint, Arena> _playerToArenaMap
= new ConcurrentDictionary<IPEndPoint, Arena>();
private Arena _nextArena;
// Used to check if we are running the server or not
private ThreadSafe<bool> _running = new ThreadSafe<bool>(false);
public PongServer(int port)
{
Port = port;
// Create the UDP socket (IPv4)
_udpClient = new UdpClient(Port, AddressFamily.InterNetwork);
}
// Notifies that we can start the server
public void Start()
{
_running.Value = true;
}
// Starts a shutdown of the server
public void Shutdown()
{
if (_running.Value)
{
Console.WriteLine("[Server] Shutdown requested by user.");
// Close any active games
Queue<Arena> arenas = new Queue<Arena>(_activeArenas.Keys);
foreach (Arena arena in arenas)
arena.Stop();
// Stops the network thread
_running.Value = false;
}
}
// Cleans up any necessary resources
public void Close()
{
_networkThread?.Join(TimeSpan.FromSeconds(10));
_udpClient.Close();
}
// Small lambda function to add a new arena
private void _addNewArena ()
{
_nextArena = new Arena(this);
_nextArena.Start();
_activeArenas.TryAdd(_nextArena, 0);
}
// Used by an Arena to notify the PingServer that it's done
public void NotifyDone(Arena arena)
{
// First remove from the Player->Arena map
Arena a;
if (arena.LeftPlayer.IsSet)
_playerToArenaMap.TryRemove(arena.LeftPlayer.Endpoint, out a);
if (arena.RightPlayer.IsSet)
_playerToArenaMap.TryRemove(arena.RightPlayer.Endpoint, out a);
// Remove from the active games hashset
byte b;
_activeArenas.TryRemove(arena, out b);
}
// Main loop function for the server
public void Run()
{
// Make sure we've called Start()
if (_running.Value)
{
// Info
Console.WriteLine("[Server] Running Ping Game");
// Start the packet receiving Thread
_networkThread = new Thread(new ThreadStart(_networkRun));
_networkThread.Start();
// Startup the first Arena
_addNewArena();
}
// Main loop of game server
bool running = _running.Value;
while (running)
{
// If we have some messages in the queue, pull them out
NetworkMessage nm;
bool have = _incomingMessages.TryDequeue(out nm);
if (have)
{
// Depending on what type of packet it is process it
if (nm.Packet.Type == PacketType.RequestJoin)
{
// We have a new client, put them into an arena
bool added = _nextArena.TryAddPlayer(nm.Sender);
if (added)
_playerToArenaMap.TryAdd(nm.Sender, _nextArena);
// If they didn't go in that means we're full, make a new arena
if (!added)
{
_addNewArena();
// Now there should be room
_nextArena.TryAddPlayer(nm.Sender);
_playerToArenaMap.TryAdd(nm.Sender, _nextArena);
}
// Dispatch the message
_nextArena.EnqueMessage(nm);
}
else
{
// Dispatch it to an existing arena
Arena arena;
if (_playerToArenaMap.TryGetValue(nm.Sender, out arena))
arena.EnqueMessage(nm);
}
}
else
Thread.Sleep(1); // Take a short nap if there are no messages
// Check for quit
running &= _running.Value;
}
}
#region Network Functions
// This function is meant to be run in its own thread
// Is writes and reads Packets to/from the UdpClient
private void _networkRun()
{
if (!_running.Value)
return;
Console.WriteLine("[Server] Waiting for UDP datagrams on port {0}", Port);
while (_running.Value)
{
bool canRead = _udpClient.Available > 0;
int numToWrite = _outgoingMessages.Count;
int numToDisconnect = _sendByePacketTo.Count;
// Get data if there is some
if (canRead)
{
// Read in one datagram
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] data = _udpClient.Receive(ref ep); // Blocks
// Enque a new message
NetworkMessage nm = new NetworkMessage();
nm.Sender = ep;
nm.Packet = new Packet(data);
nm.ReceiveTime = DateTime.Now;
_incomingMessages.Enqueue(nm);
//Console.WriteLine("RCVD: {0}", nm.Packet);
}
// Write out queued
for (int i = 0; i < numToWrite; i++)
{
// Send some data
Tuple<Packet, IPEndPoint> msg;
bool have = _outgoingMessages.TryDequeue(out msg);
if (have)
msg.Item1.Send(_udpClient, msg.Item2);
//Console.WriteLine("SENT: {0}", msg.Item1);
}
// Notify clients of Bye
for (int i = 0; i < numToDisconnect; i++)
{
IPEndPoint to;
bool have = _sendByePacketTo.TryDequeue(out to);
if (have)
{
ByePacket bp = new ByePacket();
bp.Send(_udpClient, to);
}
}
// If Nothing happened, take a nap
if (!canRead && (numToWrite == 0) && (numToDisconnect == 0))
Thread.Sleep(1);
}
Console.WriteLine("[Server] Done listening for UDP datagrams");
// Wait for all arena's thread to join
Queue<Arena> arenas = new Queue<Arena>(_activeArenas.Keys);
if (arenas.Count > 0)
{
Console.WriteLine("[Server] Waiting for active Areans to finish...");
foreach (Arena arena in arenas)
arena.JoinThread();
}
// See which clients are left to notify of Bye
if (_sendByePacketTo.Count > 0)
{
Console.WriteLine("[Server] Notifying remaining clients of shutdown...");
// run in a loop until we've told everyone else
IPEndPoint to;
bool have = _sendByePacketTo.TryDequeue(out to);
while (have)
{
ByePacket bp = new ByePacket();
bp.Send(_udpClient, to);
have = _sendByePacketTo.TryDequeue(out to);
}
}
}
// Queues up a Packet to be send to another person
public void SendPacket(Packet packet, IPEndPoint to)
{
_outgoingMessages.Enqueue(new Tuple<Packet, IPEndPoint>(packet, to));
}
// Will queue to send a ByePacket to the specified endpoint
public void SendBye(IPEndPoint to)
{
_sendByePacketTo.Enqueue(to);
}
#endregion // Network Functions
#region Program Execution
public static PongServer server;
public static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
{
// Do a graceful shutdown
args.Cancel = true;
server?.Shutdown();
}
public static void Main(string[] args)
{
// Setup the server
int port = 6000;//int.Parse(args[0].Trim());
server = new PongServer(port);
// Add the Ctrl-C handler
Console.CancelKeyPress += InterruptHandler;
// Run it
server.Start();
server.Run();
server.Close();
}
#endregion // Program Execution
}
}
// Filename: ThreadSafe.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (https://unlicense.org/)
using System;
namespace PongGame
{
// Makes one variable thread safe
public class ThreadSafe<T>
{
// The data & lock
private T _value;
private object _lock = new object();
// How to get & set the data
public T Value
{
get
{
lock (_lock)
return _value;
}
set {
lock (_lock)
_value = value;
}
}
// Initializes the value
public ThreadSafe(T value = default(T))
{
Value = value;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment