1. 程式人生 > >C#(WPF)實現拳皇(一)

C#(WPF)實現拳皇(一)

       這個遊戲還算比較大的,所以我打算分幾篇文章來介紹。本節先介紹基礎的P1的實現。其實對遊戲有所瞭解的都知道格鬥遊戲算是遊戲型別中比較難編寫的,無論是邏輯還是複雜的技能控制還有碰撞檢測都是比較複雜的,所以我儘量做到完美。

 <Canvas Name="MyCanvas" ></Canvas>

首先我們在主介面選取的佈局是Canvas的佈局,為什麼呢,因為這個佈局在我們家在圖片資源的時候是非常方便的,可以準確的控制位置,便於我們進行屬性動畫的製作。(如果這裡不懂可以先自己百度學習WPF的佈局學習,其中Grid,Canvas,StackPanel,DockPanel等都是比較重要的,後面我會更新相關的文章)。然後定義一個Image的物件當作P1:


下面我們就來看如何進行主角的載入,我們知道在一般的格鬥遊戲中,角色都不是靜止的站立的,都是會晃動的,也就以為著我們必須載入成動畫的樣子。

下面我們來學習在WPF中的動畫是怎麼實現的:

       1.時間容器的方式(TimeLine可能更準確的翻譯是時間線,但是我更喜歡時間容器的翻譯)

因為這種方式是規定一個時間段,這就是時間容器的容量,然後把動作加入進這個時間容器,最後再把這個時間容器載入進入就可以實現動畫效果了,這種一般是實現屬性動畫。什麼叫做屬性動畫呢,首先我們知道每個物件都有各種的屬性,比如說個按鈕的大小(這就叫做屬性),按鈕的位置(這就叫做屬性),按鈕的顏色等等。所以屬性動畫就是在一個規定的時間內動態的實現物件屬性變化的動畫。

 public partial class MainWindow : Window
    {
        Rectangle rect;
        public MainWindow()
        {
            InitializeComponent();
            rect = new Rectangle();
            rect.Stroke = Brushes.Black;
            rect.Fill = Brushes.Red;
            //這兩句一定要有,雖然沒有在預設的情況下是載入到(0,0)但是沒有這兩句設定這個屬性,在下面
            rect.SetValue(Canvas.LeftProperty,0D);
            rect.SetValue(Canvas.TopProperty,0D);
            rect.Height = 100;
            rect.Width = 100;
            MyCanvas.Children.Add(rect);
        }

        private void MyCanvas_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Point MoveTO = new Point();
            MoveTO = e.GetPosition(MyCanvas);

            //故事畫板,上面可以同時裝載多個時間容器
            Storyboard MyStory = new Storyboard();
            //X軸的移動
            DoubleAnimation MyXAnimation = new DoubleAnimation(MoveTO.X, TimeSpan.FromMilliseconds(2000));
            Storyboard.SetTargetProperty(MyXAnimation, new PropertyPath(Canvas.LeftProperty));
            MyStory.Children.Add(MyXAnimation);
            MyStory.Begin(rect);
            //Y軸的移動
            DoubleAnimation MyYAnimation = new DoubleAnimation(MoveTO.Y, TimeSpan.FromMilliseconds(2000));
            Storyboard.SetTargetProperty(MyYAnimation, new PropertyPath(Canvas.TopProperty));
            MyStory.Children.Add(MyYAnimation);
            MyStory.Begin(rect);
        }

這段程式碼是實現滑鼠點選方塊就動畫的移動到點選的位置,但是如果你只是這樣寫你會發現一個問題,只在你點選方塊的內部會產生移動而在點選外面是沒有效果的,為什麼呢?因為我們加入Canvas的時候,如果不對其進行操作是隻認為只有當前控制元件佔領的區域有效。解決辦法有:

1.在Xaml中加入一個無效的背景色那麼就可以實現效果了。

<Canvas Name="MyCanvas" Background="White" MouseDown="MyCanvas_MouseDown"/>

2.那就不用Canvas中的滑鼠點選函式,而用窗體的滑鼠點選函式。這兩種方法的實現效果都是一樣的,當然屬性動畫是不止DoubleAnimation的還有其他的屬性,比如PointAnimation等(不過一般都可以用DoubleAnimation來組合),想要深入瞭解可以上微軟的官網看相關的文件(當然我後續也會更新)。

       以上的動畫效果我們學會了的話,那我們就可以完成主角的走動了,沒錯,很簡單吧!主角的走動只不過是圖片的位置的移動罷了,和上面的矩形是一個效果。但是光有角色的走動好似不行的,我們還需要在走動的時候不停的實現畫面的走動效果,還有技能效果。那麼下面我們來介紹第二種動畫產生的方法。

       2.定時器產生的動畫。

在WPF中定時器有Timer和DispatcherTimer兩種定時器,但是建議最好使用DispatcherTimer這個定時器。如果學過視覺化程式設計並且進行過計時器使用的應該都知道是個什麼東西。顧名思義就是在一個規定的時間內不停的觸發一個動作,在WPF裡面就是在規定的時間間隔呼叫一個函式,完成一個規定的動作。

       public MainWindow()
        {
            InitializeComponent();
            Spirit = new Image();
            Canvas.SetLeft(Spirit,0D);
            Canvas.SetBottom(Spirit,150D);
            MyCanvas.Children.Add(Spirit);
            ResourceAdd();                                                                   
          
            //站立的計時器
            DispatcherTimer StandTimer = new DispatcherTimer();
            StandTimer.Interval = TimeSpan.FromMilliseconds(80);
            StandTimer.Tick += new EventHandler(StandTimer_Tick);
            StandTimer.IsEnabled = true;
            StandTimer.Start();
計時器如果要不停的關停開啟的話我建議在載入函式或者這個函式裡面申請,或者申請為全域性變數。因為這樣可以避免我們在其他函式裡面不停的申請,防止造成不可控制的錯誤,這樣我們只需要設定其IsEnable屬性或者呼叫Stop()函式即可控制計時器的關停和開啟。
 private void StandTimer_Tick(Object sender,EventArgs s)
        {
            if (!Is_Using_Skill)
            {
                switch (Go_Type)
                {
                    case 0:                                                                           //站立
                        {
                            for (int i = 0; i < Stand_Total_Count; i++)
                            {
                                if (i == Stand_Real_Count)
                                {
                                    Spirit.Source = Stand[i];
                                    Stand_Real_Count++;
                                    if (Stand_Real_Count == Stand_Total_Count)
                                        Stand_Real_Count = 0;
                                    break;
                                }
                            }//end of for
                            break;
                        }// end of case 0
                    case 1:                                                                         //前進
                        {
                            for (int i = 0; i < Go_Ahead_Total_Count; i++)
                            {
                                if (i == Go_Ahead_Real_Count)
                                {
                                    Spirit.Source = Go_Ahead_List[i];
                                    Go_Ahead_Real_Count++;
                                    if (Go_Ahead_Real_Count == Go_Ahead_Total_Count)
                                        Go_Ahead_Real_Count = 0;
                                    break;
                                }
                            }//end of for
                            break;
                        }//end of case 1
                    case 2:                                                                          //後退
                        {
                            for (int i = 0; i < Go_Back_Total_Count; i++)
                            {
                                if (i == Go_Back_Real_Count)
                                {
                                    Spirit.Source = Go_Back_List[i];
                                    Go_Back_Real_Count++;
                                    if (Go_Back_Real_Count == Go_Back_Total_Count)
                                        Go_Back_Real_Count = 0;
                                    break;
                                }
                            }//end of for
                            break;
                        }//End of case 2
                }//end of switch
            }
        }

然後我們要根據一個型別值來載入不同的動畫,比如走動呀,站立呀,退後等。這個邏輯的實現前提當然我們要吧資源載入進入程式,我們建立一個單獨的角色資源載入函式。

 public void ResourceAdd()
        {
            //站立資源載入
            Uri StandUri = new Uri("Image/CT_Stand.gif", UriKind.RelativeOrAbsolute);//(第二個引數指定Uri地址)
            GifBitmapDecoder Stand_decoder2 = new GifBitmapDecoder(StandUri, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            foreach (BitmapSource temp in Stand_decoder2.Frames)
            {
                Stand.Add(temp);
                Stand_Total_Count++;                                     //記錄載入的總幀數
            }

這只是一種載入資源的方法,因為我的圖片是Gif的,所以呼叫了一個Gif解碼器。如果你的圖片資源是jpg或者png等直接就可以載入,也就是你可以不用這個函式,你可以在使用的時候直接把圖片資源載入進入,比如:

Spirit.Source = new BitmapImage(new Uri("xxx"+ImageNumber.Tostring()+".xxx"),UriKin.Relative);

其中你可以使用ImageNumber來控制要載入哪一幀進入記憶體。不過就大多數情況來說一般格鬥遊戲的幀數都比較多,所以大多數資源別人都是直接打包成一個Gif,當然你也可以用一些圖片軟體吧png等的一系列圖片轉成Gif,當然你有興趣可以自己做出一個,工作量也沒你想象那麼大。所以你如果是Gif的話,請記住上面的函式,這是非常的精彩!其中第二句解碼器的引數問題,你根據需要進行選擇,都是控制載入的一些方式的,需要根據你的資源和你的需求進行不同的選擇。

       然後我們就是要實現通過按鍵來進行角色的移動,摺痕簡單我們新增一個按鍵的相應事件就可以了。

            if (Key_Down_Times <= 2 && !Is_Using_Skill)
            {
                StandTimer.IsEnabled = true;
                Point MoveTo = new Point(Canvas.GetLeft(Spirit), Canvas.GetTop(Spirit));
                Storyboard Go_Story = new Storyboard();
                if (e.Key == Key.Left && Canvas.GetLeft(Spirit) > 0)
                {
                    Go_Type = Go_Back;
                    MoveTo.X -= Go_Speed;
                    DoubleAnimation BackAnimation = new DoubleAnimation(MoveTo.X, TimeSpan.FromMilliseconds(500));
                    Storyboard.SetTargetProperty(BackAnimation, new PropertyPath(Canvas.LeftProperty));
                    Go_Story.Children.Add(BackAnimation);
                    Go_Story.Begin(Spirit);
                    Key_Down_Times++;
                }
                else if (e.Key == Key.Right && Canvas.GetLeft(Spirit) < ActualWidth)
                {
                    Go_Type = Go_Ahead;
                    MoveTo.X += Go_Speed;
                    DoubleAnimation AheadAnimation = new DoubleAnimation(MoveTo.X, TimeSpan.FromMilliseconds(500));
                    Storyboard.SetTargetProperty(AheadAnimation, new PropertyPath(Canvas.LeftProperty));
                    Go_Story.Children.Add(AheadAnimation);
                    Go_Story.Begin(Spirit);
                    Key_Down_Times++;
                }
            }// end of if
沒有什麼好解釋的,就是上面屬性動畫的運用,然後同時定時器在一直不停的運作,就產生了移動的效果。其中if的兩個判斷條件是一些邊界的判斷,比如不能移過螢幕(當然現在還沒有實現地圖的滾動效果,後面會新增的!)

       WPF還有一種實現動畫效果的實現方法,但是鑑於本遊戲可能用不著就先不介紹了,(之後會製作一個類似傳奇的遊戲,裡面我會介紹),這些動畫的實現各有各自的優點。以上大概的就可以實現一個簡單的P1的基本功能了。下面我們來進行一些優化:

      問題一:你會發現,你試試一直按下前進按鍵會產生什麼果?沒錯,人物會越來移動得越快,為什麼會這樣呢?原因是,按鍵這個響應事件是沒有限制的我們是可以一直響應的,那麼它就一直移動,就會造成這個錯誤。那麼解決辦法呢其實也很簡單,我們可以設定一個bool變數來控制,我們可以再KeyDown這個響應事件中把這個bool變數設定一個值,然後在KeyUp變數裡面改變它的值,那麼可以實現一直按下只響應一次,擡起來在按下才有效果。

       但是我們發現這樣又有一個問題,那就是在格鬥遊戲的時候我們一直按下一個按鍵它是會一直朝著一個方向移動的,而不一定是按下擡起按下擡起,因為這樣遊戲的體驗感就會差很多。那我們怎麼實現一直按下但是角色不會突然一下加速而是以一個平穩的速度移動呢?

       對,也許你應該已經想到了,我們如果能在第一個動畫效果執行完畢的時候就開始執行下一個動作就行了,那麼我們只需要根據時間的間隔來控制就行了。

解決方法1、設定一個DateTime型別的LastSecond變數,從每次動畫的執行開始記住那個時間點,然後在每次按鍵的時候再通過DateTime.Now獲取當前的時間,進行差計算度過的時間間隔,這樣就可以實現我們預期的效果。

解決方法2、再設定一個定時器,單獨用來計時。

        問題2、角色移動或者釋放技能的時候會出現抖動效果,或者釋放有些本來不應該移動位置的技能角色出現了位移的效果。其實這個原因是因為你資源的問題,因為畫素的不同,載入進來的圖片的高寬不同,而我們開始是用的Spirit.SetValue(Canvas.TopProperty,xD)這個是控制角色的上面,這樣就會產生角色的向下位移。

解決方法1、利用圖片操作的一些軟體修改圖片的畫素。

解決方法2、設定成Spirit.SetValue(Canvas.BottomProperty,xD),因為格鬥遊戲是沒有向下移動的。        

        問題3、在角色釋放技能的時候角色同時在執行走動效果,從而產生不停的鬼畜的問題。這是因為兩個定時器的衝突而產生的,因為兩個定時器都在進行運作,都在載入自己的資源幀,那麼當然會出現這種問題。

方法:關停移動的計時器,釋放技能的時候單獨設定一個計時器,設定方法和移動一樣,然後在進入僅能釋放的時候關閉移動計時器,在技能釋放到最後一幀的時候關閉技能計時器,啟動移動計時器。

        問題4、角色技能的釋放或者移動得越越快,比如第一次釋放同一個技能只使用了3秒鐘,然後第二次用了1.5秒,然後第三次1秒鐘。

解決方法:出現這種問題,多半好是你多次new了計時器,然後啟動,所以解決辦法就是在初始化的時候new出計時器,然後在需要啟動或者關停的時候利用isEnable這個屬性來完成。

       問題5、在載入資源的時候報錯,說找不到資源。

解決方法:我發現在使用Vs2013的時候你把圖片直接複製進入程式,但是程式通過相對路徑如果找不到資源的話,那麼你就需要手動吧圖片複製到bin/debug這個目錄下面去就行了。

注:技能的組合鍵的釋放效果後面會實現。