From: Charles Date: Sat, 10 Apr 2010 07:39:18 +0000 (+0000) Subject: Implemented the base architecture we are bound to because of how the network code... X-Git-Url: https://git.dogcows.com/gitweb?p=chaz%2Fcarfire;a=commitdiff_plain;h=f58af70a5768c1d99ca535fb214565ba226f3f0f Implemented the base architecture we are bound to because of how the network code works. It's kind of like a walking skeleton... or a limping skeleton. git-svn-id: https://bd85.net/svn/cs3505_group@57 92bb83a3-7c8f-8a45-bc97-515c4e399668 --- diff --git a/CarFire/CarFire/CarFire.suo b/CarFire/CarFire/CarFire.suo index 8ebdb70..dd09ba1 100644 Binary files a/CarFire/CarFire/CarFire.suo and b/CarFire/CarFire/CarFire.suo differ diff --git a/CarFire/CarFire/CarFire/CarFire.csproj b/CarFire/CarFire/CarFire/CarFire.csproj index 994f3b3..9d05f39 100644 --- a/CarFire/CarFire/CarFire/CarFire.csproj +++ b/CarFire/CarFire/CarFire/CarFire.csproj @@ -70,6 +70,7 @@ False + False @@ -83,9 +84,15 @@ + + + + + - + + diff --git a/CarFire/CarFire/CarFire/ChatInfo.cs b/CarFire/CarFire/CarFire/ChatInfo.cs new file mode 100644 index 0000000..fa6c0d1 --- /dev/null +++ b/CarFire/CarFire/CarFire/ChatInfo.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework.Net; + +namespace CarFire +{ + /// + /// Small container class for the information concerning a chat packet. + /// It is immutable. + /// + public class ChatInfo + { + // Private member variables + #region Instance Variables + + NetworkGamer mSender; + String mMessage; + + #endregion + + + /// + /// Get the sender who sent the chat packet. + /// + public NetworkGamer Sender + { + get { return mSender; } + } + + /// + /// Get the message that was sent by the sender. + /// + public String Message + { + get { return mMessage; } + } + + + /// + /// Construct a chat packet with contents. + /// + /// The chat sender. + /// The chat message. + public ChatInfo(NetworkGamer sender, String message) + { + mSender = sender; + mMessage = message; + } + } +} diff --git a/CarFire/CarFire/CarFire/Game.cs b/CarFire/CarFire/CarFire/Game.cs new file mode 100644 index 0000000..1650634 --- /dev/null +++ b/CarFire/CarFire/CarFire/Game.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace CarFire +{ + class Game : IDeterministicGame + { + #region IDeterministicGame Members + + public void LoadContent(ContentManager contentManager) + { + } + + public void UnloadContent() + { + } + + public Vector2 PreferredScreenSize + { + get { return new Vector2(800, 600); } + } + + public int MinimumSupportedPlayers + { + get { return 1; } + } + + public int MaximumSupportedPlayers + { + get { return 4; } + } + + public void ResetGame(object[] playerIdentifiers, object thisPlayer) + { + } + + public long CurrentFrameNumber + { + get { return 0; } + } + + public long CurrentChecksum + { + get { return 0; } + } + + public void ApplyKeyInput(object playerIdentifier, Keys key, bool isKeyPressed) + { + } + + public void ApplyMouseLocationInput(object playerIdentifier, int x, int y) + { + } + + public void ApplyMouseButtonInput(object playerIdentifier, bool isButtonPressed) + { + } + + public bool IsGameOver(object playerIdentifier) + { + return true; + } + + public bool IsTerminated(object playerIdentifier) + { + return true; + } + + public long Update(TimeSpan timespan) + { + return CurrentFrameNumber; + } + + public long Draw(SpriteBatch spriteBatch) + { + return CurrentFrameNumber; + } + + #endregion + } +} diff --git a/CarFire/CarFire/CarFire/IDeterministicGame.cs b/CarFire/CarFire/CarFire/IDeterministicGame.cs new file mode 100644 index 0000000..857f57a --- /dev/null +++ b/CarFire/CarFire/CarFire/IDeterministicGame.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Content; + +namespace CarFire +{ + /// + /// A DeterministicGame object is a full XNA game, except that it does not + /// extend the Microsoft.Xna.Framework.Game class. It supports content loading + /// and unloading, as well as modified Update and Draw functionality. + /// + /// DeterministicGame objects are intented to be incorporated inside of an + /// existing game. By simply calling update and draw at the appropriate times, + /// and by supplying user inputs, the game will play just like any other game. + /// + /// It is intended that a DeterministicGame be a multiplayer game, and support for + /// this is listed in the interface below. Each player is identified by a unique object + /// reference (of the caller's choice, not a struct). The game supports the notion of a + /// current 'frame', or state. The enclosing code supplies the user inputs for the + /// next frame by calling methods. The enclosing code then should call the update + /// method to advance the game to the next frame. Finally, the enclosing code + /// calls the draw method to render the game state. Note that the game state can + /// be drawn multiple times without updating the game, thus allowing the game + /// to be paused or stalled. + /// + public interface IDeterministicGame + { + /// + /// Call this method to give the game a chance to load its content. + /// + /// A valid content manager pointing to the root of the content tree + void LoadContent (ContentManager contentManager); + + /// + /// Call this method to give the game a chance to unload its content. + /// + void UnloadContent(); + + /// + /// Returns the preferred screen size for this game. + /// + /// + Vector2 PreferredScreenSize { get; } + + /// + /// Returns the minimum number of players this game can support. + /// + /// the minimum player count + int MinimumSupportedPlayers { get; } + + /// + /// Returns the maximum number of players this game can support. + /// + /// the maximum player count + int MaximumSupportedPlayers { get; } + + /// + /// Call this method to reset the game state, to set the current frame at 0, and + /// to supply identifiers for each player in the game. Player identifiers should + /// be unique object references (not structs) that the caller will use later + /// to identify each player. (It is important that these not be 'boxed' object + /// references or the reference will not be preserved.) + /// + /// Since, in theory, there will be four copies of the game running, a second + /// parameter identifies the player that is running this copy of the game. + /// + /// An array of objects (references) that will identify each player + /// An object identifier for the player whose machine is displaying this game + void ResetGame(Object[] playerIdentifiers, Object thisPlayer); + + /// + /// Returns the current frame number. This corresponds to the current state + /// of the game world. + /// + /// the current frame number + long CurrentFrameNumber { get; } + + /// + /// Returns a checksum of all of the game world state. This checksum can be used + /// to ensure that multiple players at some frame all share the same state. It is + /// guaranteed that identical states will produce identical checksums. + /// + /// the current game state checksum + long CurrentChecksum { get; } + + /// + /// Call this method to report changes in keypresses to the game. You should call this method + /// to report any changes in keyboard state for a player. The keyboard state will be + /// applied to the next game state (not the current state). + /// + /// An object (reference) that was registered for a player in the game + /// A key identifier + /// The key state - true means pressed, false means released + void ApplyKeyInput (Object playerIdentifier, Keys key, bool isKeyPressed); + + /// + /// Call this method to report changes in mouse locations to the game. You should call this method + /// any time the mouse coordinates for a player changes. The mouse information will + /// be applied to the next game state (not the current state). + /// + /// an object (reference) that was registered for a player in the game + /// the mouse x location + /// the mouse y location + void ApplyMouseLocationInput (Object playerIdentifier, int x, int y); + + /// + /// Call this method to report changes in mouse button state to the game. Note that only one + /// mouse button is supported in game. You should call this method to report any + /// changes in mouse button state for a player. The mouse button state will be + /// applied to the next game state (not the current state). + /// + /// an object (reference) that was registered for a player in the game + /// the mouse button state + void ApplyMouseButtonInput (Object playerIdentifier, bool isButtonPressed); + + /// + /// Returns true if the specified player's game is over. They can be safely disconnected from the game + /// when this flag is true, their inputs do not affect game state. (You can continue to report inputs, + /// to allow the player to view a game over screen, but no game state action is taken.) + /// + /// an object (reference) that was registered for a player in the game + /// true if the game is over + bool IsGameOver(Object playerIdentifier); + + /// + /// Returns true if the specified player's game is over, and the player has clicked on something indicating + /// they wish to leave the game over screen. (This only becomes true if inputs are reported + /// even after the game is over.) + /// + /// an object (reference) that was registered for a player in the game + /// true if the player has terminated the game + bool IsTerminated(Object playerIdentifier); + + /// + /// Call this method to advance the game state. Previously sent inputs are applied + /// to the game state and the frame number is advanced and returned. Caution should be used when + /// supplying the seconds parameter - it can affect game state. All players in a game + /// should advance their game time by the same amount. + /// + /// The elapsed game time + /// the frame number of the new game state (now the current state) + long Update(TimeSpan timespan); + + /// + /// Draws the current game state. This does not affect the game state - it may be called + /// repeatedly to redraw the current game state if needed. + /// + /// a SpriteBatch object that has begun a batch + /// the current game state frame number + long Draw(SpriteBatch spriteBatch); + } +} diff --git a/CarFire/CarFire/CarFire/IScreenManager.cs b/CarFire/CarFire/CarFire/IScreenManager.cs new file mode 100644 index 0000000..1b15e20 --- /dev/null +++ b/CarFire/CarFire/CarFire/IScreenManager.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace CarFire +{ + public interface IScreenManager + { + void LoadContent(ContentManager contentManager, GraphicsDeviceManager graphics); + void UnloadContent(); + long Update(GameTime gameTime, NetworkManager networkGame); + long Draw(SpriteBatch spriteBatch); + } +} diff --git a/CarFire/CarFire/CarFire/NetworkManager.cs b/CarFire/CarFire/CarFire/NetworkManager.cs new file mode 100644 index 0000000..3fd4ca4 --- /dev/null +++ b/CarFire/CarFire/CarFire/NetworkManager.cs @@ -0,0 +1,1065 @@ + +// Make sure DEBUG is undefined when turning in the project +// or the grader will wonder why it's so laggy. +#undef DEBUG + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework.Net; +using System.Diagnostics; +using Microsoft.Xna.Framework.GamerServices; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using System.Collections; + +namespace CarFire +{ + /// + /// A manager class to handle network interactions between peers and + /// lobby/game switching. + /// + public class NetworkManager + { + // Public methods and properties + #region Public Methods + + /// + /// Called when a session has been created or joined using CreateSession() or JoinSession(). + /// + /// The new session that was created or joined. + /// The NetworkGame that joined the session. + public delegate void JoinedSessionDelegate(NetworkSession session, NetworkManager networkGame); + + /// + /// Called when sessions are found as a result of calling FindSessions(). + /// + /// A container of the available sessions. + /// The NetworkGame that searched for the sessions. + public delegate void FoundSessionsDelegate(AvailableNetworkSessionCollection sessions, NetworkManager networkGame); + + + /// + /// Called when an exception is thrown during an asynchronous operation. + /// + /// The exception that was thrown. + /// The NetworkGame that errored. + public delegate void CaughtErrorDelegate(Exception exception, NetworkManager networkGame); + + /// + /// Get and set the error delegate, called when an exception is thrown during + /// and asynchronous operation. This will occur if you try to create or join a + /// session without being logged into a profile. + /// + public CaughtErrorDelegate ErrorDelegate; + + + /// + /// Construct a NetworkGame with a lobby and a game. + /// + /// Provides an associated lobby to update and draw. + /// Provides a game object to be played over the network. + public NetworkManager(IScreenManager lobby, IDeterministicGame game) + { + Debug.Assert(lobby != null && game != null); + + mLobby = lobby; + mGame = game; + } + + + /// + /// Get the Gamer object for the local player. + /// + public LocalNetworkGamer LocalGamer + { + get + { + // TODO: Is this the correct way to get the single local gamer? + return mNetworkSession.LocalGamers[0]; + } + } + + /// + /// Get all the gamers associated with the active network session. + /// + public GamerCollection NetworkGamers + { + get + { + return mNetworkSession.AllGamers; + } + } + + + /// + /// Begin a new network session with the local gamer as the host. You must not + /// call this method or use JoinSession without first using LeaveSession. + /// + /// The delegate/method to call when the session is created. + public void CreateSession(JoinedSessionDelegate callback) + { + CreateSession(mGame.MaximumSupportedPlayers, callback); + } + + /// + /// Begin a new network session with the local gamer as the host. You must not + /// call this method or use JoinSession without first using LeaveSession. + /// + /// Provide the maximum number of players allowed to connect. + /// The delegate/method to call when the session is created. + public void CreateSession(int maxGamers, JoinedSessionDelegate callback) + { + Debug.Assert(mNetworkSession == null); + + mJoinedSessionDelegate = callback; + NetworkSession.BeginCreate(NetworkSessionType.SystemLink, 1, maxGamers, CreateSessionEnd, null); + } + void CreateSessionEnd(IAsyncResult result) + { + Debug.Assert(mNetworkSession == null); + + try + { + mNetworkSession = NetworkSession.EndCreate(result); + mNetworkSession.AllowHostMigration = true; + mNetworkSession.AllowJoinInProgress = false; + mNetworkSession.GameStarted += new EventHandler(GameStartedEvent); + } + catch (Exception e) + { + if (ErrorDelegate != null) ErrorDelegate(e, this); + return; + } + mJoinedSessionDelegate(mNetworkSession, this); + mJoinedSessionDelegate = null; + } + void GameStartedEvent(object sender, GameStartedEventArgs e) + { + Reset(); + } + + /// + /// Determine whether or not the network game object is associated with any network session. + /// + /// True if there exists a NetworkSession; false otherwise. + public bool HasActiveSession + { + get + { + return mNetworkSession != null; + } + } + + + /// + /// Find available sessions to join. You should not already be in a session when + /// calling this method; call LeaveSession first. + /// + /// The delegate/method to call when the search finishes. + public void FindSessions(FoundSessionsDelegate callback) + { + Debug.Assert(mNetworkSession == null); + + mFoundSessionsDelegate = callback; + NetworkSession.BeginFind(NetworkSessionType.SystemLink, 1, null, new AsyncCallback(FindSessionsEnd), null); + } + void FindSessionsEnd(IAsyncResult result) + { + AvailableNetworkSessionCollection sessions; + try + { + sessions = NetworkSession.EndFind(result); + } + catch (Exception e) + { + if (ErrorDelegate != null) ErrorDelegate(e, this); + return; + } + mFoundSessionsDelegate(sessions, this); + mFoundSessionsDelegate = null; + } + + /// + /// Join a network session found using FindSessions(). This is for joining a game that + /// somebody else has already started hosting. You must not already be in a session. + /// + /// Pass the session object to try to join. + /// The delegate/method to call when the search finishes. + public void JoinSession(AvailableNetworkSession availableSession, JoinedSessionDelegate callback) + { + Debug.Assert(mNetworkSession == null); + + mJoinedSessionDelegate = callback; + NetworkSession.BeginJoin(availableSession, JoinSessionEnd, null); + } + void JoinSessionEnd(IAsyncResult result) + { + Debug.Assert(mNetworkSession == null); + + try + { + mNetworkSession = NetworkSession.EndJoin(result); + mNetworkSession.GameStarted += new EventHandler(GameStartedEvent); + } + catch (Exception e) + { + if (ErrorDelegate != null) ErrorDelegate(e, this); + return; + } + mJoinedSessionDelegate(mNetworkSession, this); + mJoinedSessionDelegate = null; + } + + + /// + /// Leave and dispose of any currently associated network session. You will find yourself + /// back in the lobby. You must already be in a session to leave it. + /// + public void LeaveSession() + { + Debug.Assert(mNetworkSession != null); + + mNetworkSession.Dispose(); + mNetworkSession = null; + } + + + /// + /// Set up the network session to simulate 200ms latency and 10% packet loss. + /// + public void SimulateBadNetwork() + { + Debug.Assert(mNetworkSession != null); + + mNetworkSession.SimulatedLatency = new TimeSpan(0, 0, 0, 0, 200); + mNetworkSession.SimulatedPacketLoss = 0.1f; + } + + + /// + /// Indicate that the game should begin (moving players from the lobby to the game). + /// You must call CreateSession() before calling this. + /// + public void StartGame() + { + Debug.Assert(mNetworkSession != null && mNetworkSession.IsHost && + mNetworkSession.AllGamers.Count >= mGame.MinimumSupportedPlayers && + mNetworkSession.IsEveryoneReady); + + ForceStartGame(); + } + + /// + /// Indicate that the game should begin. This is like StartGame() without the sanity + /// checks. Use this for debugging. + /// + public void ForceStartGame() + { + mNetworkSession.StartGame(); + mNetworkSession.ResetReady(); + } + + + /// + /// Manages the network session and allows either the lobby or game to update. + /// + /// Pass the time away. + public void Update(GameTime gameTime) + { + if (mNetworkSession == null) + { + mLobby.Update(gameTime, this); + } + else + { + mNetworkSession.Update(); + HandleIncomingPackets(); + + if (mNetworkSession.SessionState == NetworkSessionState.Lobby) + { + mLobby.Update(gameTime, this); + } + else if (mNetworkSession.SessionState == NetworkSessionState.Playing) + { + if (mGame.IsTerminated(LocalGamerInfo)) + { + LeaveSession(); + return; + } + else if (mGame.IsGameOver(LocalGamerInfo)) + { + ApplyEvents(LocalGamerInfo, GetEventsFromInput()); + mGame.Update(mTargetTimeSpan); + return; + } + + if (HaveNeededEvents) + { + if (IsLatencyAdjustmentFrame) + { + AdjustLatency(); + mLastStallCount = mStallCount; + mStallCount = 0; + } + mLocalEvents.AddRange(GetEventsFromInput()); + SendLocalEvents(); + ApplyEvents(); + +#if DEBUG + Console.WriteLine("HASH: " + mGame.CurrentFrameNumber + "\t" + mGame.CurrentChecksum); +#endif + + mGame.Update(mTargetTimeSpan); + } + else // Stall! + { + mStallCount++; + + // Send a reliable event packet to each stalled gamer. + if (mStallCount == 1) + { +#if DEBUG + Console.WriteLine("STAL: ===="); +#endif + + foreach (GamerInfo gamerInfo in GamerArray) + { + if (gamerInfo.HighestFrameNumber < mGame.CurrentFrameNumber) + { + SendLocalEvents(gamerInfo.Gamer); + } + } + } + else if (mStallCount > 600) + { + Console.WriteLine("One or more players have stalled excessively. Leaving session..."); + LeaveSession(); + } + } + } + } + } + + /// + /// Allows either the lobby or the game to draw, depending on the state + /// of the network connection and whether or not a game is in progress. + /// + /// Pass the time away. + /// The sprite batch. + public void Draw(GameTime gameTime, SpriteBatch spriteBatch) + { + if (mNetworkSession == null) + { + mLobby.Draw(spriteBatch); + } + else + { + if (mNetworkSession.SessionState == NetworkSessionState.Lobby) + { + mLobby.Draw(spriteBatch); + } + else if (mNetworkSession.SessionState == NetworkSessionState.Playing) + { + mGame.Draw(spriteBatch); + } + } + } + + + /// + /// Get the chat messages that have been received since the last time this + /// method was called. + /// + /// List container of the chat messages. + public List ReceiveChats() + { + List chats = mChatPackets; + mChatPackets = new List(); + return chats; + } + + /// + /// Send a chat message to all gamers in the session. You should already be + /// in a session before calling this method. + /// + /// The text of the message. + public void SendChat(String message) + { + WriteChatPacket(message); + LocalGamer.SendData(mPacketWriter, SendDataOptions.ReliableInOrder); + } + + /// + /// Send a chat message to a specific gamer in the session. You should already + /// be in a session before calling this method. + /// + /// The text of the message. + /// The gamer to receive the message. + public void SendChat(String message, NetworkGamer recipient) + { + Debug.Assert(recipient != null && !recipient.IsDisposed); + + WriteChatPacket(message); + LocalGamer.SendData(mPacketWriter, SendDataOptions.ReliableInOrder, recipient); + } + + #endregion + + + // Private class variable members + #region Instance Variables + + NetworkSession mNetworkSession; + PacketReader mPacketReader = new PacketReader(); + PacketWriter mPacketWriter = new PacketWriter(); + + JoinedSessionDelegate mJoinedSessionDelegate; + FoundSessionsDelegate mFoundSessionsDelegate; + + IScreenManager mLobby; + IDeterministicGame mGame; + + List mChatPackets = new List(); + + List mLocalEvents = new List(); + List mLastLocalEvents = new List(); + + List mLastPressedKeys = new List(); + bool mLastLeftButtonPressed; + bool mLastRightButtonPressed; + bool mLastMiddleButtonPressed; + int mLastMousePositionX; + int mLastMousePositionY; + + int mLatency; + long mHighestFrameNumber; + long mNextLatencyAdjustmentFrame; + int mStallCount; + int mLastStallCount; + int mAverageOwd; + +#if DEBUG + bool mDontSendEvents; +#endif + + TimeSpan mTargetTimeSpan = new TimeSpan(166666); + public TimeSpan TargetTimeSpan + { + get + { + return mTargetTimeSpan; + } + } + + Dictionary mGamers; + GamerInfo[] GamerArray + { + get + { + GamerInfo[] gamerList = mGamers.Values.ToArray(); + Array.Sort(gamerList, delegate(GamerInfo a, GamerInfo b) + { + return a.Gamer.Id.CompareTo(b.Gamer.Id); + }); + return gamerList; + } + } + GamerInfo LocalGamerInfo + { + get + { + return mGamers[LocalGamer.Id]; + } + } + + #endregion + + + // Private types for the implementation of the network protocol + #region Private Types + + enum PacketType + { + Chat = 1, + Event = 2 + } + + enum EventType + { + KeyDown = 1, + KeyUp = 2, + MouseDown = 3, + MouseUp = 4, + MouseMove = 5 + } + + enum MouseButton + { + Left = 1, + Right = 2, + Middle = 3 + } + + abstract class EventInfo + { + public NetworkGamer Gamer; + public long FrameOfApplication; + + public EventInfo(NetworkGamer gamer, long frameNumber) + { + Gamer = gamer; + FrameOfApplication = frameNumber; + } + + public abstract EventType Id + { + get; + } + } + + class KeyboardEventInfo : EventInfo + { + public Keys Key; + public bool IsKeyDown; + + public KeyboardEventInfo(NetworkGamer gamer, long frameNumber, Keys key, bool isDown) + : base(gamer, frameNumber) + { + Key = key; + IsKeyDown = isDown; + } + + public override EventType Id + { + get { return IsKeyDown ? EventType.KeyDown : EventType.KeyUp; } + } + } + + class MouseButtonEventInfo : EventInfo + { + public MouseButton Button; + public bool IsButtonDown; + + public MouseButtonEventInfo(NetworkGamer gamer, long frameNumber, MouseButton button, bool isDown) + : base(gamer, frameNumber) + { + Button = button; + IsButtonDown = isDown; + } + + public override EventType Id + { + get { return IsButtonDown ? EventType.MouseDown : EventType.MouseUp; } + } + } + + class MouseMotionEventInfo : EventInfo + { + public int X; + public int Y; + + public MouseMotionEventInfo(NetworkGamer gamer, long frameNumber, int x, int y) + : base(gamer, frameNumber) + { + X = x; + Y = y; + } + + public override EventType Id + { + get { return EventType.MouseMove; } + } + } + + class GamerInfo + { + public NetworkGamer Gamer; + public long HighestFrameNumber = 0; + public int StallCount = 0; + public int AverageOwd = 0; + public int NextStallCount = 0; + public int NextAverageOwd = 0; + public bool IsWaitedOn = false; + public List[] Events = new List[MaximumLatency]; + + public GamerInfo(NetworkGamer gamer) + { + Gamer = gamer; + } + } + + const int MaximumLatency = 120; + const int StallTimeout = 900; + + #endregion + + + // Private implementation methods of the network protocol + #region Private Implementation Methods + + /// + /// Reinitialize the private variables in preparation for a new game to start. + /// + void Reset() + { + mLatency = 1; + mHighestFrameNumber = 0; + mNextLatencyAdjustmentFrame = 1; + mStallCount = 0; + mLastStallCount = 0; + mAverageOwd = CurrentAverageOneWayDelay; + + mGamers = new Dictionary(); + foreach (NetworkGamer gamer in NetworkGamers) + { + mGamers.Add(gamer.Id, new GamerInfo(gamer)); + } + + mGame.ResetGame(GamerArray, LocalGamerInfo); + } + + + void HandleIncomingPackets() + { + LocalNetworkGamer localGamer = LocalGamer; + + while (localGamer.IsDataAvailable) + { + NetworkGamer sender; + + localGamer.ReceiveData(mPacketReader, out sender); + + PacketType packetId = (PacketType)mPacketReader.ReadByte(); + switch (packetId) + { + case PacketType.Chat: + + short messageLength = mPacketReader.ReadInt16(); + char[] message = mPacketReader.ReadChars(messageLength); + + ChatInfo chatPacket = new ChatInfo(sender, new String(message)); + mChatPackets.Add(chatPacket); + break; + + case PacketType.Event: + + GamerInfo senderInfo = mGamers[sender.Id]; + + int stallCount = mPacketReader.ReadInt16(); + int averageOwd = mPacketReader.ReadInt16(); + int frameNumber = mPacketReader.ReadInt32(); + int numEvents = mPacketReader.ReadByte(); + + if (frameNumber <= mNextLatencyAdjustmentFrame) + { + senderInfo.StallCount = stallCount; + senderInfo.AverageOwd = averageOwd; + } + else + { + senderInfo.NextStallCount = stallCount; + senderInfo.NextAverageOwd = averageOwd; + } + + if (frameNumber <= senderInfo.HighestFrameNumber) + { +#if DEBUG + Console.WriteLine("SKP" + (char)sender.Id + ": " + mGame.CurrentFrameNumber + "\t" + frameNumber + "\t<=\t" + senderInfo.HighestFrameNumber + "\t#" + numEvents); +#endif + + // we know about all these events, so don't bother reading them + break; + } + +#if DEBUG + Console.WriteLine(" GOT" + (char)sender.Id + ": " + mGame.CurrentFrameNumber + "\t" + frameNumber + "\t>\t" + senderInfo.HighestFrameNumber + "\t#" + numEvents); +#endif + + for (int i = 0; i < numEvents; i++) + { + EventInfo eventInfo = ReadEvent(mPacketReader, sender); + + if (eventInfo != null && eventInfo.FrameOfApplication > senderInfo.HighestFrameNumber) + { + int index = GetEventArrayIndexForFrame(eventInfo.FrameOfApplication); + if (senderInfo.Events[index] == null) senderInfo.Events[index] = new List(); + senderInfo.Events[index].Add(eventInfo); + } + } + + senderInfo.HighestFrameNumber = frameNumber; + break; + + default: + + Console.WriteLine("Received unknown packet type: " + (int)packetId); + break; + } + } + } + + + int CurrentEventArrayIndex + { + get { return GetEventArrayIndexForFrame(mGame.CurrentFrameNumber); } + } + + int GetEventArrayIndexForFrame(long frame) + { + return (int)(frame % MaximumLatency); + } + + EventInfo ReadEvent(PacketReader packetReader, NetworkGamer sender) + { + EventType eventId = (EventType)packetReader.ReadByte(); + long frameNumber = packetReader.ReadInt32(); + + switch (eventId) + { + case EventType.KeyDown: + + Keys keyCode1 = (Keys)packetReader.ReadInt32(); + return new KeyboardEventInfo(sender, frameNumber, keyCode1, true); + + case EventType.KeyUp: + + Keys keyCode2 = (Keys)packetReader.ReadInt32(); + return new KeyboardEventInfo(sender, frameNumber, keyCode2, false); + + case EventType.MouseDown: + + MouseButton buttonId1 = (MouseButton)packetReader.ReadByte(); + return new MouseButtonEventInfo(sender, frameNumber, buttonId1, true); + + case EventType.MouseUp: + + MouseButton buttonId2 = (MouseButton)packetReader.ReadByte(); + return new MouseButtonEventInfo(sender, frameNumber, buttonId2, false); + + case EventType.MouseMove: + + short x = packetReader.ReadInt16(); + short y = packetReader.ReadInt16(); + return new MouseMotionEventInfo(sender, frameNumber, x, y); + + default: + + Console.WriteLine("Received unknown event type: " + (int)eventId); + return null; + } + } + + + void WriteChatPacket(String message) + { + mPacketWriter.Write((byte)PacketType.Chat); + mPacketWriter.Write((short)message.Length); + mPacketWriter.Write(message.ToCharArray()); + } + + void WriteEventPacket(List events, long highestFrameNumber) + { + mPacketWriter.Write((byte)PacketType.Event); + mPacketWriter.Write((short)mLastStallCount); + mPacketWriter.Write((short)mAverageOwd); + mPacketWriter.Write((int)highestFrameNumber); + mPacketWriter.Write((byte)events.Count); + + foreach (EventInfo eventInfo in events) + { + mPacketWriter.Write((byte)eventInfo.Id); + mPacketWriter.Write((int)eventInfo.FrameOfApplication); + + KeyboardEventInfo keyboardEventInfo = eventInfo as KeyboardEventInfo; + if (keyboardEventInfo != null) + { + mPacketWriter.Write((int)keyboardEventInfo.Key); + continue; + } + + MouseButtonEventInfo mouseButtonEventInfo = eventInfo as MouseButtonEventInfo; + if (mouseButtonEventInfo != null) + { + mPacketWriter.Write((byte)mouseButtonEventInfo.Button); + continue; + } + + MouseMotionEventInfo mouseMotionEventInfo = eventInfo as MouseMotionEventInfo; + if (mouseMotionEventInfo != null) + { + mPacketWriter.Write((short)mouseMotionEventInfo.X); + mPacketWriter.Write((short)mouseMotionEventInfo.Y); + continue; + } + } + } + + + bool IsLatencyAdjustmentFrame + { + get + { + return mNextLatencyAdjustmentFrame == mGame.CurrentFrameNumber; + } + } + + void AdjustLatency() + { + Debug.Assert(IsLatencyAdjustmentFrame); + +#if DEBUG + if (mStallCount > 0) + { + Console.WriteLine("STL#: " + mGame.CurrentFrameNumber + "\t" + mStallCount); + } +#endif + + int maxStallCount = 0; + int maxAverageOwd = 0; + + foreach (GamerInfo gamerInfo in GamerArray) + { + if (gamerInfo.StallCount > maxStallCount) maxStallCount = gamerInfo.StallCount; + if (gamerInfo.AverageOwd > maxAverageOwd) maxAverageOwd = gamerInfo.AverageOwd; + + gamerInfo.StallCount = gamerInfo.NextStallCount; + gamerInfo.AverageOwd = gamerInfo.NextAverageOwd; + } + +#if DEBUG + int prevLatency = mLatency; +#endif + + if (maxStallCount > 0) + { + mLatency += maxStallCount; + } + else + { + mLatency -= (int)(0.6 * (double)(mLatency - maxAverageOwd) + 1.0); + } + + if (mLatency < 1) mLatency = 1; + if (mLatency > MaximumLatency) mLatency = MaximumLatency; + +#if DEBUG + if (prevLatency != mLatency) Console.WriteLine("NLAG: " + mLatency); +#endif + + mNextLatencyAdjustmentFrame = mGame.CurrentFrameNumber + mLatency; + mAverageOwd = CurrentAverageOneWayDelay; + + mLastLocalEvents = mLocalEvents; + mLocalEvents = new List(); + } + + + + List GetEventsFromInput() + { + List events = new List(); + + long frameOfApplication = mGame.CurrentFrameNumber + mLatency; + if (frameOfApplication <= mHighestFrameNumber) return events; + else mHighestFrameNumber = frameOfApplication; + + // 1. Find the keyboard differences; written by Peter. + + KeyboardState keyState = Keyboard.GetState(); + + List pressedKeys = new List(); + List releasedKeys = new List(); + + Keys[] pressedKeysArray = keyState.GetPressedKeys(); + foreach (Keys k in pressedKeysArray) + { + if (!mLastPressedKeys.Contains(k)) pressedKeys.Add(k); + else mLastPressedKeys.Remove(k); + } + + releasedKeys = mLastPressedKeys; + + foreach (Keys key in pressedKeys) + { + events.Add(new KeyboardEventInfo(LocalGamer, frameOfApplication, key, true)); + } + foreach (Keys key in releasedKeys) + { + events.Add(new KeyboardEventInfo(LocalGamer, frameOfApplication, key, false)); + } + +#if DEBUG + if (pressedKeys.Contains(Keys.Escape)) mDontSendEvents = true; + if (releasedKeys.Contains(Keys.Escape)) mDontSendEvents = false; +#endif + + // 2. Find the mouse differences. + + MouseState mouseState = Mouse.GetState(); + + bool leftButtonPressed = mouseState.LeftButton == ButtonState.Pressed; + if (leftButtonPressed != mLastLeftButtonPressed) + { + events.Add(new MouseButtonEventInfo(LocalGamer, frameOfApplication, MouseButton.Left, leftButtonPressed)); + } + + bool rightButtonPressed = mouseState.RightButton == ButtonState.Pressed; + if (rightButtonPressed != mLastRightButtonPressed) + { + events.Add(new MouseButtonEventInfo(LocalGamer, frameOfApplication, MouseButton.Right, rightButtonPressed)); + } + + bool middleButtonPressed = mouseState.MiddleButton == ButtonState.Pressed; + if (middleButtonPressed != mLastMiddleButtonPressed) + { + events.Add(new MouseButtonEventInfo(LocalGamer, frameOfApplication, MouseButton.Middle, middleButtonPressed)); + } + + int mousePositionX = mouseState.X; + int mousePositionY = mouseState.Y; + if (mousePositionX != mLastMousePositionX || mousePositionY != mLastMousePositionY) + { + events.Add(new MouseMotionEventInfo(LocalGamer, frameOfApplication, mousePositionX, mousePositionY)); + } + + // 3. Save the current peripheral state. + + mLastPressedKeys = new List(pressedKeysArray); + mLastLeftButtonPressed = leftButtonPressed; + mLastRightButtonPressed = rightButtonPressed; + mLastMiddleButtonPressed = middleButtonPressed; + mLastMousePositionX = mousePositionX; + mLastMousePositionY = mousePositionY; + + return events; + } + + void SendLocalEvents() + { + SendLocalEvents((NetworkGamer)null); + } + + void SendLocalEvents(List recipicents) + { + foreach (NetworkGamer gamer in recipicents) + { + SendLocalEvents(gamer); + } + } + + void SendLocalEvents(NetworkGamer recipient) + { +#if DEBUG + if (mDontSendEvents) return; +#endif + + List events = new List(mLocalEvents); + events.AddRange(mLastLocalEvents); + + if (recipient != null && !recipient.IsDisposed) + { + // if there is a recipient, we are resending old events + WriteEventPacket(events, mGame.CurrentFrameNumber - 1); + LocalGamer.SendData(mPacketWriter, SendDataOptions.Reliable, recipient); + } + else + { + WriteEventPacket(events, mGame.CurrentFrameNumber + mLatency); + LocalGamer.SendData(mPacketWriter, SendDataOptions.None); + } + } + + + bool HaveNeededEvents + { + get + { + long currentFrame = mGame.CurrentFrameNumber; + + foreach (GamerInfo gamerInfo in mGamers.Values) + { + if (mGame.IsGameOver(gamerInfo)) continue; + if (gamerInfo.HighestFrameNumber < currentFrame) return false; + } + + return true; + } + } + + void ApplyEvents() + { + int index = CurrentEventArrayIndex; + + foreach (GamerInfo gamerInfo in GamerArray) + { + if (gamerInfo.Events[index] == null) continue; + ApplyEvents(gamerInfo, gamerInfo.Events[index]); + gamerInfo.Events[index] = null; + } + } + + void ApplyEvents(GamerInfo gamerInfo, List events) + { + foreach (EventInfo eventInfo in events) + { + KeyboardEventInfo keyboardEventInfo = eventInfo as KeyboardEventInfo; + if (keyboardEventInfo != null) + { +#if DEBUG + Console.WriteLine(" KEY: " + keyboardEventInfo.FrameOfApplication + "\t" + keyboardEventInfo.Key + "," + keyboardEventInfo.IsKeyDown); +#endif + + mGame.ApplyKeyInput(gamerInfo, keyboardEventInfo.Key, keyboardEventInfo.IsKeyDown); + continue; + } + + MouseButtonEventInfo mouseButtonEventInfo = eventInfo as MouseButtonEventInfo; + if (mouseButtonEventInfo != null) + { +#if DEBUG + Console.WriteLine(" BTN: " + mouseButtonEventInfo.FrameOfApplication + "\t" + mouseButtonEventInfo.IsButtonDown); +#endif + + mGame.ApplyMouseButtonInput(gamerInfo, mouseButtonEventInfo.IsButtonDown); + continue; + } + + MouseMotionEventInfo mouseMotionEventInfo = eventInfo as MouseMotionEventInfo; + if (mouseMotionEventInfo != null) + { +#if DEBUG + Console.WriteLine(" MMV: " + mouseMotionEventInfo.FrameOfApplication + "\t" + mouseMotionEventInfo.X + "," + mouseMotionEventInfo.Y); +#endif + + mGame.ApplyMouseLocationInput(gamerInfo, mouseMotionEventInfo.X, mouseMotionEventInfo.Y); + continue; + } + } + } + + + int CurrentAverageOneWayDelay + { + get + { + Debug.Assert(mNetworkSession != null); + + double numRemoteGamersTwice = 2 * mNetworkSession.RemoteGamers.Count; + double averageOwd = 0; + + foreach (NetworkGamer gamer in mNetworkSession.RemoteGamers) + { + TimeSpan timeSpan = gamer.RoundtripTime; + averageOwd += timeSpan.TotalMilliseconds; + } + + return (int)((averageOwd / numRemoteGamersTwice) / 16.6666); + } + } + + #endregion + } +} diff --git a/CarFire/CarFire/CarFire/Program.cs b/CarFire/CarFire/CarFire/Program.cs index 7dad1ae..8a00895 100644 --- a/CarFire/CarFire/CarFire/Program.cs +++ b/CarFire/CarFire/CarFire/Program.cs @@ -9,7 +9,7 @@ namespace CarFire /// static void Main(string[] args) { - using (Game1 game = new Game1()) + using (XnaGame game = new XnaGame()) { game.Run(); } diff --git a/CarFire/CarFire/CarFire/ScreenManager.cs b/CarFire/CarFire/CarFire/ScreenManager.cs new file mode 100644 index 0000000..5c6bf71 --- /dev/null +++ b/CarFire/CarFire/CarFire/ScreenManager.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace CarFire +{ + class ScreenManager : IScreenManager + { + #region ILobby Members + + public void LoadContent(ContentManager contentManager, GraphicsDeviceManager graphics) + { + } + + public void UnloadContent() + { + } + + public long Update(GameTime gameTime, NetworkManager networkGame) + { + return 0; + } + + public long Draw(SpriteBatch spriteBatch) + { + return 0; + } + + #endregion + } +} diff --git a/CarFire/CarFire/CarFire/Game1.cs b/CarFire/CarFire/CarFire/XnaGame.cs similarity index 65% rename from CarFire/CarFire/CarFire/Game1.cs rename to CarFire/CarFire/CarFire/XnaGame.cs index 0b9d9fe..0623ae9 100644 --- a/CarFire/CarFire/CarFire/Game1.cs +++ b/CarFire/CarFire/CarFire/XnaGame.cs @@ -16,15 +16,30 @@ namespace CarFire /// /// This is the main type for your game /// - public class Game1 : Microsoft.Xna.Framework.Game + public class XnaGame : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; - public Game1() + NetworkManager networkGame; + IScreenManager screenManager; + IDeterministicGame deterministicGame; + + public XnaGame() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; + + Components.Add(new GamerServicesComponent(this)); + + screenManager = new ScreenManager(); + deterministicGame = new Game(); + networkGame = new NetworkManager(screenManager, deterministicGame); + + Vector2 size = deterministicGame.PreferredScreenSize; + graphics.PreferredBackBufferWidth = (int)size.X; + graphics.PreferredBackBufferHeight = (int)size.Y; + graphics.ApplyChanges(); } /// @@ -35,7 +50,8 @@ namespace CarFire /// protected override void Initialize() { - // TODO: Add your initialization logic here + IsFixedTimeStep = true; + TargetElapsedTime = networkGame.TargetTimeSpan; base.Initialize(); } @@ -49,7 +65,8 @@ namespace CarFire // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); - // TODO: use this.Content to load your game content here + screenManager.LoadContent(Content, graphics); + deterministicGame.LoadContent(Content); } /// @@ -58,7 +75,8 @@ namespace CarFire /// protected override void UnloadContent() { - // TODO: Unload any non ContentManager content here + screenManager.UnloadContent(); + deterministicGame.UnloadContent(); } /// @@ -68,11 +86,7 @@ namespace CarFire /// Provides a snapshot of timing values. protected override void Update(GameTime gameTime) { - // Allows the game to exit - if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) - this.Exit(); - - // TODO: Add your update logic here + networkGame.Update(gameTime); base.Update(gameTime); } @@ -83,9 +97,11 @@ namespace CarFire /// Provides a snapshot of timing values. protected override void Draw(GameTime gameTime) { - GraphicsDevice.Clear(Color.CornflowerBlue); + GraphicsDevice.Clear(Color.Red); - // TODO: Add your drawing code here + spriteBatch.Begin(); + networkGame.Draw(gameTime, spriteBatch); + spriteBatch.End(); base.Draw(gameTime); }