Click here to Skip to main content
15,894,343 members
Articles / Artificial Intelligence

Writing a Multiplayer Game (in WPF)

Rate me:
Please Sign up or sign in to vote.
4.93/5 (131 votes)
16 Mar 2012CPOL25 min read 216.2K   17.1K   246  
This article will explain some concepts of game development and how to apply and adapt them for multiplayer development.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Pfz.RemoteGaming.Internal;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// This class represents a Room in the server game.
	/// Changes to remote game properties are seen by everyone that is in the same room.
	/// Usually games start with exclusive rooms (so only client and server comunicate) and then
	/// the Participant joins another (common) room.
	/// </summary>
	public class RemoteGameRoom:
		ThreadSafeExceptionAwareDisposable
	{
		#region Fields
			internal static long _idGenerator;
			internal HashSet<RemoteGameComponent> _addedComponents = new HashSet<RemoteGameComponent>();
			internal HashSet<RemoteGameComponent> _removedComponents = new HashSet<RemoteGameComponent>();
			internal Dictionary<long, RemoteGameComponent> _components = new Dictionary<long, RemoteGameComponent>();
			internal bool _started;
			private IEnumerator<bool> _animation;
			private HighPrecisionTimer _timer;
			internal static long _participantIdGenerator;
		#endregion

		#region Constructor
			/// <summary>
			/// Creates a new room.
			/// </summary>
			public RemoteGameRoom()
			{
				MustDisposeIfEmpty = true;
			}
		#endregion
		#region Dispose
			/// <summary>
			/// Disposes the actual room.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				if (disposing)
				{
					Disposer.Dispose(ref _timer);
					Disposer.Dispose(ref _animation);

					var components = _components;
					if (components != null)
					{
						foreach(var component in components.Values)
							component.Dispose();

						_components = null;
					}

					_addedComponents = null;
					_removedComponents = null;
				}

				base.Dispose(disposing);
			}
		#endregion

		#region Properties
			#region _RoomLock
				internal object _RoomLock
				{
					get
					{
						return base.DisposeLock;
					}
				}
			#endregion

			#region MustDisposeIfEmpty
				/// <summary>
				/// Gets or sets a value telling if the room must be disposed if there are no Participants in it.
				/// The default is true.
				/// </summary>
				public bool MustDisposeIfEmpty { get; set; }
			#endregion
			#region ParticipantCount
				/// <summary>
				/// Gets the number of participants in this room.
				/// </summary>
				public int ParticipantCount
				{
					get
					{
						lock(DisposeLock)
						{
							CheckUndisposed();

							return _participants.Count;
						}
					}
				}
			#endregion
			#region UpdateTime
				/// <summary>
				/// Gets or sets the interval time used between frames.
				/// A value of 0 means that there will be no updates.
				/// </summary>
				public TimeSpan UpdateTime
				{
					get
					{
						var timer = _timer;
						if (timer == null)
							return TimeSpan.Zero;

						return timer.Interval;
					}
				}
			#endregion
		#endregion
		#region Methods
			#region CheckUndisposed
				internal new void CheckUndisposed()
				{
					base.CheckUndisposed();
				}
			#endregion

			#region _CreateComponent
				internal T _CreateComponent<T>(RemoteGameParticipant owner)
				where
					T: RemoteGameComponent
				{
					var result = (T)_AssemblyGenerator.GetConstructor(typeof(T)).Invoke(null);
					try
					{
						result._roomLock = DisposeLock;
						lock(_RoomLock)
						{
							CheckUndisposed();
							result._room = this;
							long id = Interlocked.Increment(ref _idGenerator);
							result._id = id;
							_addedComponents.Add(result);
							_components.Add(id, result);
							result.IsPublic = true;

							if (owner != null)
								result.Owner = owner;

							result.OnInitialize();
						}
					}
					catch
					{
						result.Dispose();
						throw;
					}

					return result;
				}
			#endregion
			#region _RemoveComponent
				internal void _RemoveComponent(RemoteGameComponent component)
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return;

						if (!_addedComponents.Remove(component))
							_removedComponents.Add(component);

						_components.Remove(component._id);
					}
				}
			#endregion
			#region _ApplyChangesImmediately
				private void _ApplyChangesImmediately()
				{
					try
					{
						_ApplyChangesImmediately(true);
					}
					catch(Exception exception)
					{
						Dispose(exception);
					}
				}
				private void _ApplyChangesImmediately(bool updateAnimation)
				{
					RemoteGameParticipant[] participants;
					RemoteGameComponent[] components;
					RemoteGameComponent[] addedComponents;
					RemoteGameComponent[] removedComponents;
					IEnumerator<bool> animation;

					lock(_RoomLock)
					{
						if (WasDisposed)
							return;

						if (_participants.Count == 0)
						{
							if (MustDisposeIfEmpty)
							{
								Dispose();
								return;
							}

							foreach (var component in _components.Values)
							{
								component._componentLock.EnterWriteLock();
								try
								{
									if (!component._wasDisposed)
										component._changes.Clear();
								}
								finally
								{
									component._componentLock.ExitWriteLock();
								}
							}

							return;
						}

						animation = _animation;
						participants = _participants.ToArray();
						components = _components.Values.ToArray();
						addedComponents = _addedComponents.ToArray();
						removedComponents = _removedComponents.ToArray();

						_addedComponents.Clear();
						_removedComponents.Clear();
					}

					if (updateAnimation && animation != null && !animation.MoveNext())
					{
						Dispose();
						return;
					}

					var volatileValues = new List<KeyValuePair<long, object[]>>();
					var componentChanges = new List<KeyValuePair<RemoteGameComponent, KeyValuePair<int, object>[]>>();
					foreach (var component in components)
					{
						var changes = component._GetChanges();
						if (changes != null)
						{
							var pair = new KeyValuePair<RemoteGameComponent, KeyValuePair<int, object>[]>(component, changes);
							componentChanges.Add(pair);
						}

						component._AddVolatileValues(volatileValues);
					}

					// we must call StoreChanges even if there are none, as this allows some participants that had their notifications
					// disabled or with private components to work properly.
					foreach (var participant in participants)
						participant._StoreChanges(componentChanges, addedComponents, removedComponents, volatileValues);
				}
			#endregion

			#region ApplyChangesImmediately
				/// <summary>
				/// Applies any changes done to this room immediately.
				/// You must call this method if the room doesn't have an animation.
				/// </summary>
				public void ApplyChangesImmediately()
				{
					_ApplyChangesImmediately(false);
				}
			#endregion
			#region CreateComponent
				/// <summary>
				/// Creates a component into this room that is not owned by any participant and is public.
				/// </summary>
				public T CreateComponent<T>()
				where
					T: RemoteGameComponent
				{
					return _CreateComponent<T>(null);
				}
			#endregion
			#region GetComponents
				/// <summary>
				/// Gets all components in this room.
				/// Note that private components are only acessible by the Participant.GetPrivateComponents.
				/// </summary>
				/// <returns>An array with the components or null if this room was disposed.</returns>
				public RemoteGameComponent[] GetComponents()
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return null;

						return _components.Values.ToArray();
					}
				}
			#endregion
			#region GetParticipants
				internal HashSet<RemoteGameParticipant> _participants = new HashSet<RemoteGameParticipant>();
				/// <summary>
				/// Gets all participants in this room.
				/// </summary>
				/// <returns>An array with the participants or null if this room was disposed.</returns>
				public RemoteGameParticipant[] GetParticipants()
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return null;

						return _participants.ToArray();
					}
				}
			#endregion
			#region Start
				#region Start()
					/// <summary>
					/// Starts this run without an animation.
					/// Use it for rooms that respond to requests only.
					/// </summary>
					public void Start()
					{
						lock(_RoomLock)
						{
							CheckUndisposed();

							if (_started)
								throw new RemoteGameException("RemoteGameRoom already started.");

							_started = true;
						}

						ApplyChangesImmediately();
					}
				#endregion
				#region Start(double interval, IEnumerator<bool> animation)
					/// <summary>
					/// Starts this room, which will run the given animation in the given interval times.
					/// </summary>
					public void Start(double interval, IEnumerator<bool> animation)
					{
						if (animation == null)
							throw new ArgumentNullException("animation");

						lock(_RoomLock)
						{
							CheckUndisposed();

							if (_started)
								throw new RemoteGameException("RemoteGameRoom already started.");

							_started = true;
							_animation = animation;
							_timer = new HighPrecisionTimer(_ApplyChangesImmediately, interval);
						}
					}
				#endregion
			#endregion

			#region RegisterAction - Many overloads
				internal Dictionary<Type, Func<RemoteGameParticipant, RemoteGameRequest, object>> _registeredActions = new Dictionary<Type, Func<RemoteGameParticipant, RemoteGameRequest, object>>();

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<RemoteGameParticipant, T, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action(participant, (T)request)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action()
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<T, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action((T)request)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<RemoteGameParticipant, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action(participant)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}
			#endregion
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions