對於自己做的游戲即使輸了臉上一般還是洋溢著笑容的,那麼如何做一個簡 單的游戲呢?(.netFramework 3.5 SP1)
一.再說呈現
在自繪那篇我們已經知道了如何把東西畫出來, 或許你已經能通過重載OnRender函數非常熟練的畫出這樣棋子。
那麼OnRender方法中的DrawingContext參數到底來自哪呢?因為 DrawingContext是抽象類,所以微軟創建了一個叫做RenderDataDrawingContext 的具體類以及他的子類VisualDrawingContext,我們的所用的DrawingContext實 際就是VisualDrawingContext這個類,不過微軟都把他們定義為了internal,我 們在程序集以外無法訪問,既然無法訪問,那麼當我們需要多個這樣的對象時如 何創建呢?
DrawingVisual這類為我們實現多個DrawingContext成為了可 能,因為他的實例方法RenderOpen()在內部創建了VisualDrawingContext, DrawingVisual也可以說繼承於Visual,當我們用DrawingContext的Drawing一些 東西的時候,其實產生的是畫圖的數據,數據有了,可要把數據給UI 的線程才 能被顯示,WPF似乎是用ContextLayoutManager這個類來把UI重繪請求放到 Dispatcher隊列,用Visual裡的DUCE發送消息和線程對話.
我們創建可視 數據的代碼可以寫成這樣:
DrawingVisual boardVisual = new DrawingVisual();
using (DrawingContext drawingContext = boardVisual.RenderOpen())
{
//畫棋盤
}
以 上是創建了畫圖的數據,那麼怎麼用ContextLayoutManager把數據給Dispatcher 隊列。UIElement中的 PropagateResumeLayout方法循環遞歸把需要刷新的 Visual對象放到隊列中,經過這樣的分裝我們只需要知道把需要呈現的Visual仍 給系統就可以,他自己會判斷是否要刷新。
怎麼給Visual,微軟要求我 們先給Visual的數量,這需要我們通過以下方式來給定
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
然後他會用一個for循環來得到需 要的Visual對象
for (int i = 0; i < internalVisualChildrenCount; i++)
{
Visual visualChild = v.InternalGetVisualChild(i);
if (visualChild != null)
{
PropagateResumeLayout(v, visualChild);
}
}
internalVisualChildrenCount的數量就是VisualChildrenCount 返回的值, InternalGetVisualChild的方法實際做的就是我們常重載的 GetVisualChild方法。
protected override Visual GetVisualChild(int index)
{
if (index == 0)
return boardVisual;
else
throw new ArgumentOutOfRangeException ("out of range");
}
就這麼簡單,我們是否 已經看到自己畫的棋盤了。
二.可視樹與邏輯樹
雖然我也承認這 兩棵樹已經被人刨根問底的掘了N次,但對於自定義控件來說這樹可不是說捨棄 就捨棄的(要充分合理利用資源嘛)。
邏輯樹(Logical Tree)當然是邏 輯意義上的,如果把Visual 可以比作汽車的一部分比如車廂,輪胎,油箱,當 我們坐在車廂裡的時候我們實際也可以說做在車中,車是一個邏輯意義上的含義 ,是各個汽車零件和的總稱。所以我們的控件上的Parent或者是Child一般都是 邏輯的物件,那麼加入他除了一個標示外,還有其他的什麼意義呢?他還可以屬 性值繼承,比如說我們在我們這個象棋控件上設置下字體的變化,希望上面的棋 子車、馬、帥等的字體也發生變化就可以用到他。
在我們的象棋控件中 這樣注冊:
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register ("FontFamily",
typeof(FontFamily),
typeof (ChineseChessboard),
new FrameworkPropertyMetadata (SystemFonts.MessageFontFamily, FrameworkPropertyMetadataOptions.Inherits));
棋子中可以這 樣
public static readonly DependencyProperty FontFamilyProperty = ChineseChessboard.FontFamilyProperty.AddOwner (typeof(ChessmanControl),
new FrameworkPropertyMetadata (SystemFonts.MessageFontFamily, FrameworkPropertyMetadataOptions.Inherits));
另外的還有事 件路由和資源就不多說了(我也沒用到^-^)。
可視樹(Visual Tree)據 說是邏輯樹的擴展,所以把元素只加入可視樹依然可以進行屬性繼承,當然前提 是類要繼承於FrameworkElement 或FrameworkContentElement,從樹的字面意義 來看似乎告訴我們不加入他就不能看到,實際上他和呈現沒有太大關系,可問題 是當你把一個visual顯示出來了,也停留在只可遠觀而不可亵玩的地步 —— 不能引發事件,加入了VisualTree 這個問題就可以解決了,有 了點擊測試(HitTest),事件也有了,人也順心了,編程也不困惑了。這裡要說明 下當我把整個大棋盤加入到可視樹時,上面的棋子未加入到可視樹的前提下,棋 子上的鼠標響應依然可以獲得HitTest也能捕捉的到棋子,可見WPF似乎根據一個 區域的像素點來判斷。至於VisualTreeHelper裡的方法可以自己查詢MSDN了解。
說到這兩個樹還有兩個類值得提下:VisualCollection和 UIElementCollection 前者可以用來操作可視樹,後者既可以操作可視樹又可以 操作邏輯樹,如果只想操作可視樹,可以把UIElementCollection 構造函數中的 logicParent 賦為null。使用了這兩個類你就可以簡單操作樹上的元素了。
三.HitTest和TranslateTransform
當我們把棋子放到 UIElementCollection 中,並輸出隊列
protected override int VisualChildrenCount
{
get
{
// +1 是為棋盤
return (ChessmanCollection != null ? ChessmanCollection.Count : 0) + 1;
}
}
protected override Visual GetVisualChild(int index)
{
//第一個為棋 盤其他為棋子
if (index == 0)
return boardVisual;
return ChessmanCollection[index - 1];
}
並輸出 他們的實際大小
protected override Size MeasureOverride (Size availableSize)
{
for (int i = 0; ChessmanCollection != null && i < ChessmanCollection.Count; i++)
{
ChessmanCollection [i].Measure(availableSize);
}
return new Size(CellWidth * 10, CellWidth * 11);
}
看到了棋子的顯示,我們接 下來的事便是要選中棋子
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
Point location = e.GetPosition(this);
HitTestResult result = VisualTreeHelper.HitTest(this, location);
ChessmanControl chessmanControl = result.VisualHit as ChessmanControl;
當然點擊測試還可以做很多事,具體的看 http://msdn.microsoft.com/zh-cn/library/ms752097.aspx
選中以後我 們要移動,並需要一個移動過程的效果
TranslateTransform _moveTransform = new TranslateTransform();
_chessmanBaseDic [newChessman].RenderTransform = _moveTransform;
DoubleAnimation xAnimation = new DoubleAnimation((newPoint.X - oldPoint.X) * CellWidth, _moveDuration, FillBehavior.Stop);
_moveTransform.BeginAnimation(TranslateTransform.XProperty, xAnimation);
DoubleAnimation yAnimation = new DoubleAnimation((newPoint.Y - oldPoint.Y) * CellWidth, _moveDuration, FillBehavior.Stop);
EventHandler tempAction = default(EventHandler);
tempAction = delegate
{
_chessmanBaseDic[newChessman].ClearValue (UIElement.RenderTransformProperty);
_chessmanBaseDic [newChessman].isSelected = false;
_currentChessmanControl = null;
if (oldChessman != null)
{
ChessmanCollection.Remove(_chessmanBaseDic[oldChessman]);
}
//更新
this.InvalidateArrange();
//移除本身
yAnimation.Completed -= tempAction;
};
yAnimation.Completed += tempAction;
_moveTransform.BeginAnimation(TranslateTransform.YProperty, yAnimation);
如果你感覺寫兩個Animation來分別控制有點蠢的 話,你也可以把TranslateTransform 放到CompositionTarget.Rendering來控制 。
當然這樣移動是暫時的,所以我們要引發InvalidateArrange()來實際 輸出控件的位置
protected override Size ArrangeOverride (Size finalSize)
{
for (int i = 0; ChessmanCollection != null && i < ChessmanCollection.Count; i++)
{
ChessmanControl item = ChessmanCollection[i] as ChessmanControl;
item.Arrange(new Rect(
new Point (item.Chessman.Location.X * CellWidth - item.DesiredSize.Width / 2+CellWidth,
item.Chessman.Location.Y * CellWidth - item.DesiredSize.Height / 2+CellWidth),
item.DesiredSize));
}
return this.DesiredSize;
}
四.畫圖問題
棋盤背景可以加張圖片
BitmapImage backgroundImage = new BitmapImage();
backgroundImage.BeginInit();
backgroundImage.UriSource = new Uri (@"pack://application:,,,/ChineseChessControl;component/Images/wo odDeskground.jpg", UriKind.RelativeOrAbsolute);
backgroundImage.EndInit();
backgroundImage.Freeze ();
其中的Uri 如果不熟悉的話可以看下面兩個網址(一個為3.5 的一個為3.0的):
http://msdn.microsoft.com/en- us/library/aa970069.aspx#The_Pack_URI_Scheme
http://msdn.microso ft.com/en-us/library/aa970069(VS.85).aspx
對於棋盤的立體效果,是 用兩條線來達到的,上面一條用了深色畫筆,下面一個用了淺色畫筆
drawingContext.DrawLine(darkPen, new Point(CellWidth * i, CellWidth), new Point(CellWidth * i, CellWidth * 10 / 2));
drawingContext.DrawLine(lightPen, new Point(CellWidth * i + 1.5, CellWidth), new Point(CellWidth * i + 1.5, CellWidth * 10 / 2));
棋盤的話也可以先畫四分之一,然 後通過反轉得到
//第一象限
drawingContext.PushTransform(new ScaleTransform(-1, 1, CellWidth * 5, CellWidth * 5.5));
quarterBoard();
drawingContext.Pop();
//第二象限
quarterBoard();
//第三象限
drawingContext.PushTransform (new ScaleTransform(1, -1, CellWidth * 5, CellWidth * 5.5));
quarterBoard();
drawingContext.Pop();
//第四象限
drawingContext.PushTransform(new ScaleTransform(-1, -1, CellWidth * 5, CellWidth * 5.5));
quarterBoard();
drawingContext.Pop();
對於楚河、漢界幾個字先文字豎排, 文字豎排其實就是限制文字輸出的寬度,讓他把字給擠下去。
FormattedText textFormat = new FormattedText(
text, System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface ("STLiti"),
fontSize,
Brushes.Black);
textFormat.MaxTextWidth = fontSize;
textFormat.TextAlignment = TextAlignment.Justify;
在翻轉的時候要注意的是我們的 翻轉的文字塊往往是長方形,而不是正方形,
所以你在需要先把A點的X坐標加上長寬之差一半的距離再加全部字寬 度的一半,Y坐標減去長寬之差的一半加上字體大小的一半再加全部字的高度的 一半
五.動畫提醒
有些時候下棋的時候會沒有覺察出對方下了哪 個棋,應該過幾秒提醒下,順便也督促下已經思考了一段時間了快快下吧。
我們可以使用 DispatcherTimer類來計時,讓其每個幾秒來做提醒。
DispatcherTimer _dispatcherTimer = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
_dispatcherTimer.Interval = TimeSpan.FromSeconds(10);
_dispatcherTimer.Tick += delegate
{
_renderingListener.StartListening();
};
然後用CompositionTarget.Rendering來改變控件的透明度達到 跳動的效果。
_renderingListener.Rendering += delegate
{
if (tempChessmanControl == null)
tempChessmanControl = _lastMoveChessman;
if (tempChessmanControl != null)
{
if (frameCount % 20 == 0)
{
tempChessmanControl.Opacity = tempChessmanControl.Opacity > 0 ? 0 : 1;
}
frameCount++;
if (tempChessmanControl != _lastMoveChessman //已經換了棋子
|| frameCount > 120 //提醒大於規定
|| !IsRedReady //紅方沒有開始,或已經結束
|| !IsBlueReady) //藍方沒有開始,或已經結束
{
tempChessmanControl.Opacity = 1;
frameCount = 0;
tempChessmanControl = null;
_renderingListener.StopListening ();
}
}
};
六.聯機操作
協議: UDP
先發送廣播給局域網看是否有空閒主機。
空閒主機接到廣播 如果空閒則回應。
創建主機的時候再發廣播告訴已經有新建主機。
互聯後告訴其他主機已經在對戰,讓其從服務列表下拿掉。
走一 步告訴對方是從哪個坐標點移動到哪個坐標點
沒做
發送廣播
public void SendBroadcast(object obj, int port)
{
byte[] sendbuf = UDPClass.Serialization(obj);
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ep = new IPEndPoint (IPAddress.Broadcast, port);
s.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
s.SendTo(sendbuf, ep);
s.Close();
}
發送普通 消息
public void Send(IPAddress ipAddress, object obj, int port)
{
byte[] sendbuf = UDPClass.Serialization(obj);
Socket s = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ep = new IPEndPoint(ipAddress, port);
s.SendTo (sendbuf, ep);
s.Close();
}
監聽端口
private void StartUdpListenerPort()
{
bool done = true;
using (UdpClient listener = new UdpClient())
{
listener.EnableBroadcast = true;
IPEndPoint iep = new IPEndPoint(IPAddress.Any, _listenPort);
//端口復用
listener.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
listener.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, false);
listener.Client.Bind(iep);//綁定這個實例
try
{
while (done)
{
byte[] bytes = listener.Receive(ref iep);
_callback(iep,_listenPort, UDPClass.Deserialize(bytes));
}
}
finally
{
listener.Close();
}
}
}
因為存在多客戶端所以一個端口不能重用,我是從2000可以,每 次加一看看是否被使用過。這裡用了比較爛的方法,不知道大家有沒有比較好的 辦法
public static int GetIdlePort(int startPort)
{
while (true)
{
Socket s = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint ipport = new IPEndPoint(IPAddress.Any, startPort);
try
{
s.Bind(ipport);
break;
}
catch
{
startPort++;
}
finally
{
s.Close();
}
}
return startPort;
}
七.窗體關閉事件
窗體的事件是在View上的,如何讓 ViewModel和之上的事件相綁定呢?我們知道事件其實包含的是一個委托的集合 ,如果讓View上的事件所用的委托同ViewModel的委托相綁定不就得的效果了。 我們可以參考Prism框架做個中間件,用附加屬性給窗體以作綁定。
<Window x:Class="ChineseChess.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentatio n"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ChineseChess"
xmlns:views="clr-namespace:ChineseChess.Views"
Width="500" Height="600">
<local:WindowRegionMetadata.WindowRegionMetadata>
<local:WindowRegionMetadata BeforeClose="{Binding PersistAction}"/>
</local:WindowRegionMetadata.WindowRegionMetadata>
<Window.Title>
<MultiBinding StringFormat="{} {0}:{1}">
<Binding Path="LocalIP"/>
<Binding Path="Port"/>
</MultiBinding>
</Window.Title>
但是 App中的啟動要改成這樣
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Shell window = WindowRegionBehavior.CreateWindow<Shell> (null, null);
ShellViewModel shellViewModel = new ShellViewModel();
window.DataContext = shellViewModel;
window.Show();
}
具體的代碼限於篇幅 請參見事例。
八.寫在最後
本事例只是簡單的實現了一些功能, 如有需要添加的可以自己練練手,做些小游戲還是很能提高對程序的積極性的。
本文配套源碼