在項目之間有段“空項期”,上個項目剛剛完成,下個項目還沒落實,時間比較充裕。去年9月份就經歷了這麼一次短暫的“空項期”,那時偶還是一名前端工作者,C#使用起來毫不含糊,還自己整過一個類SCSF的MVP框架AngelFrame(詳見之前博客:http://www.cnblogs.com/wgp13x/p/99c2adc52d8f0dff30a038841ac32872.html)。在那段“空項期”之前,有位朋友托我做個小游戲,偶也滿口的答應,只可惜之前項目太忙沒時間做,就一直耽擱了,正好有這段“空項期”,所以做了一下,現在回想起來,做這個小游戲的過程中還是學習到了不少東西的,因為做游戲跟做項目的常用技術不同,於是在這裡總結一下,別把這麼寶貴的經驗給弄丟了。
這個小游戲的需求很簡單,就是在局域網的環境裡能夠自組織一個飛行棋平台,多個玩家在裡面你一步我一步的玩,看誰先飛完全程。在做這個游戲之前,偶連什麼是飛行棋,飛行棋怎麼玩的都不懂,就先在網上試玩了一小把,查了查飛行棋的規則。會玩兒了,就要想想怎麼做了,規則實現肯定要有,用戶交互是個問題。在網上搜索了一下現成的C#飛行棋實現例子,發現不是給自家女兒做的就是給自家兒子做的,單機版的,手機版的,就是沒有局域網版的。好吧,只有看我來創造了。
關鍵詞:飛行棋, C#, 局域網, 多人對戰
摘要:很久之前就有個朋友托我做個游戲了,這個游戲的需求很簡單,就是在局域網的環境裡能夠自組織一個飛行棋平台,多個玩家在裡面你一步我一步的玩。游戲規則有了,實現的難點在於自建多人對戰平台,C#飛行棋實現例子有很多,可就是沒有局域網版的,下面就是我抽出時間寫的一個局域網多人對戰飛行棋,在這裡總結一下。
先看一下我做的游戲的運行界面,網上有個單機版本的飛行棋,我借鑒了它的界面及游戲邏輯,由於它是開源的,也不知道源代碼提供者是誰,這裡就不做相關鏈接了。
解釋一下,打開主界面就是上面這個樣子,如果局域網內同時在線的游戲客戶端多,那麼在當前在線列表中會顯示同時在線的客戶端IP和用戶名,這時你先選擇要進行游戲的客戶端,再點擊創建游戲,就可以開創一輪新游戲了,游戲者在兩到四之間。如果你要添加跨網段的客戶端,那就要點擊邀請好友按鈕,填寫IP,如果它們在線就會添加到當前在線列表中,這時你再選擇它們,點擊創建游戲,就能夠在開創的新游戲裡跟他們玩了。
創建游戲成功後,在左下角處會出現一個色子,游戲面板上會出現各類顏色的飛機,每個游戲者對應一類顏色。由一個游戲者先擲色子,擲到6後才能夠起飛,其它的游戲者輪循著來,每個游戲者的動作都能夠被其它游戲者收到,在下方文字欄中,會作出說明。
網絡版的游戲,要做只能做成一個Server和多個Client,每個Client的每一步都要通知Server,由Server來運行游戲邏輯,指導Client的運行流程;或只是多個Client,每個Client都要維護其它的Client信息,Client的運行流程都要通知其它所有的Client,每個Client都運行游戲邏輯。我這裡陰差陽錯的選擇了第二種,每個Client都是平等的,沒有Server。通知用的是UDP,局域網的用戶交互就全靠它了,下面是網絡通知相關的代碼,使用單例模式,它是局域網游戲運行起來的核。
public class Network
{
private static Network _instace;
private const int Port = 100;
public static UdpClient UdpClient;
private static Encoding encoding = Encoding.GetEncoding("gb2312");
public static string HostName;
public static IPAddress IpAddress;
private static Thread listener;
public event EventHandler<GameMsg> ReceivedMsg;
public static Network Instance
{
get
{
if (_instace == null)
_instace = new Network();
return _instace;
}
}
private Network()
{
UdpClient = new UdpClient(Port);
HostName = Dns.GetHostName();
foreach (IPAddress ip in Dns.GetHostAddresses(HostName))
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
IpAddress = ip;
break;
}
}
}
//局域網內廣播,在上線時調用,通知其它Client有人上線了
public void Broadcast(GameMsg msg)
{
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Broadcast, Port);
byte[] bytes = Tools.Serialize(msg);
UdpClient.Send(bytes, bytes.Length, ipEndPoint);
}
//接收消息線程入口,收到消息後觸發各類事件
public void ReceiveMsg()
{
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Broadcast, Port);
while (true)
{
byte[] bytes = null;
try
{
bytes = UdpClient.Receive(ref ipEndPoint);
}
catch (SocketException ex)
{
return;
}
GameMsg msg = (GameMsg)Tools.Deserialize(bytes);
if (ReceivedMsg != null && !msg.IpAddress.Equals(IpAddress))
ReceivedMsg(this, msg);
}
}
//向某IP定向發送消息
public void Send(IPAddress ip, GameMsg msg)
{
IPEndPoint ipEndPoint = new IPEndPoint(ip, Port);
byte[] bytes = Tools.Serialize(msg);
UdpClient.Send(bytes, bytes.Length, ipEndPoint);
}
//向一些IP定向發送消息
public void Send(IPAddress[] ips, GameMsg msg)
{
foreach (IPAddress ipAddress in ips)
{
if (!ipAddress.Equals(IpAddress))
Send(ipAddress, msg);
}
}
}
下面是游戲中,Client之間需要交互的一些消息體定義。GameMsg是消息體,裡面包含消息類型,IP地址,消息內容,消息內容有可能是LandGameMsg對象,也有可能是CreateGameMsg對象...
[Serializable]
public class GameMsg : EventArgs
{
public MsgTypeEnum MsgType;
public IPAddress IpAddress;
public object MsgContent;
}
[Serializable]
public class LandGameMsg //OnlineReply, Hello
{
public string HostName;
public string UserName;
}
[Serializable]
public class CreateGameMsg
{
public string CreaterHostName;
public string CreaterUserName;
public IPAddress[] SelectedIpAddresses;
public string[] SelectedHostNames;
}
......
在主界面中,有些對接收到的消息共同的處理邏輯,在這裡也列出來給大家看一下吧。
public partial class MainForm : Form
{
public static Thread Listener = new Thread(new ThreadStart(Network.Instance.ReceiveMsg)) { Name = "receiveMsg", Priority = ThreadPriority.Highest };
public static CurrStatEnum CurrStat = CurrStatEnum.Idle;
private static readonly MsgTypeEnum[] interestMsgTypes = new MsgTypeEnum[] { MsgTypeEnum.QuitGame };
public MainForm()
{
Listener.Start();
Network.Instance.ReceivedMsg += new EventHandler<GameMsg>(_network_ReceivedMsg);
Network.Instance.Broadcast(new GameMsg() { MsgType = MsgTypeEnum.LandGame, IpAddress = Network.IpAddress, MsgContent = new LandGameMsg() { HostName = Network.HostName } });
}
private delegate void Delegate_ReceivedMsg(GameMsg msg);
void _network_ReceivedMsg(object sender, GameMsg e)
{
Delegate_ReceivedMsg myDelegate = new Delegate_ReceivedMsg(handleReceivedMsg);
if (interestMsgTypes.Contains(e.MsgType))
Invoke(myDelegate, e);
}
void handleReceivedMsg(GameMsg msg)
{
switch (msg.MsgType)
{
case MsgTypeEnum.QuitGame: //收到某人退出游戲請求
MessageBox.Show("有小伙伴要求退出游戲");
ucUsersInGame_QuitGame(null, null);
break;
}
}
}
如上所示,在主界面中主要是對用戶退出游戲請求做出一些邏輯處理,這裡的線程監控網絡發來的所有消息,但只對退出游戲請求感興趣,提示用戶,本局游戲結束。
在其它界面也是類似的過程,下面再列出邀請好友界面裡的,對其它游戲者的反饋信息做出邏輯處理的代碼段。
public partial class InviteOthers : Form
{
private static readonly MsgTypeEnum[] interestMsgTypes = new MsgTypeEnum[] { MsgTypeEnum.OnlineReply };
public List<GameMsg> OnlineReplys = new List<GameMsg>();
void _network_ReceivedMsg(object sender, GameMsg msg)
{
if (interestMsgTypes.Contains(msg.MsgType))
{
switch (msg.MsgType)
{
case MsgTypeEnum.OnlineReply:
OnlineReplys.Add(msg);
break;
}
}
}
}
游戲引擎,游戲邏輯處理在這裡就不多列了,那是在單機飛行棋裡的實現了的。
游戲的基本功能實現了後,還實現了一些添喜的功能,比如隱藏到桌面上方、下方、旁邊,就是你可以把游戲窗口拖動到屏幕的最上方,然後松鼠標,游戲會縮到屏幕上方,當你再把鼠標移動到屏幕上方時,它還會出來,就跟掛QQ的功能一樣。它是這樣實現的,在主界面中添加3個Windows.Forms.Timer,timer1的Enabled=True,Interval=100;timer2、timer3的Enabled=False,Interval=1,再各自添加如下事件處理邏輯。
/// 監控鼠標和窗口位置
private void timer1_Tick(object sender, EventArgs e)
{
int mouse_x = Cursor.Position.X, mouse_y = Cursor.Position.Y;
int window_x = this.Location.X, window_y = this.Location.Y;
int window_width = this.Size.Width, window_height = this.Size.Height;
if (isHiding == false && window_y == 0)
{
if (window_x - mouse_x > 10 || mouse_x - window_x - window_width > 10
|| mouse_y - window_y - window_height > 10)
{
timer1.Enabled = false;
timer2.Enabled = true;
}
}
if (isHiding == true && mouse_y <= 1 && mouse_x > window_x &&
mouse_x < window_x + window_width)
{
timer1.Enabled = false;
timer3.Enabled = true;
}
}
/// 隱藏界面
private void timer2_Tick(object sender, EventArgs e)
{
int window_height = this.Size.Height;
startY += window_height / 8;
if (startY < window_height)
{
this.Location = new Point(this.Location.X, -startY);
}
else
{
this.Location = new Point(this.Location.X, 1 - window_height);
isHiding = true;
timer2.Enabled = false;
timer1.Enabled = true;
}
}
/// 顯示界面
private void timer3_Tick(object sender, EventArgs e)
{
int window_height = this.Size.Height;
startY -= window_height / 8;
if (startY > 0)
{
this.Location = new Point(this.Location.X, -startY);
}
else
{
this.Location = new Point(this.Location.X, 0);
isHiding = false;
timer3.Enabled = false;
timer1.Enabled = true;
}
}
就這樣把局域網多人對戰飛行棋給實現了,回看這次的編程設計經歷,覺得這種P2P式的,無Server式的游戲設計是個問題。這種設計影響到游戲編碼方式,使得游戲編碼雜亂無章,沒有一個主心骨來對游戲步驟進行統一管理,這樣如若數據在網絡中丟失,很容易導致多客戶端不同步的現象。應該在選取完游戲伙伴創建新游戲時,自主選擇一個Server來處理游戲邏輯,這樣,8個人、10個人、更多的人同時在線都可以自組織游戲平台了,不同步也可以避免了。
其它的一些收獲:
1、System.Windows.Forms.Application.DoEvents();可以督促主線程處理當前在消息隊列中的所有Windows消息。
2、朋友機器上的操作系統是WinXP,自帶的沒有.net4.0,自帶.net3.0,剛開始運行不起來,後來全部換到.net3.0調試、改代碼,才運行得起來。
3、做完後,在家裡跟老婆兩個人打對戰,玩了一晚上這個游戲都不累,還挺好玩的。
好久沒來博客園更新博客了,最近新項目來了。新項目做完,又有好多新知識可以總結喽!
來自為知筆記(Wiz)