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这个目录下面去就行了。

注:技能的组合键的释放效果后面会实现。

#include #include #include #include #include #define N 100 int n=0; int Checktoseek(char name[]);//查找 void mainmenu();//菜单 void Register();//登记注册 void Showplayer();//显示当前人物数据 void Showallplayer();//显示所有人物数据 void Chooseopponent();//选择对手 void Chooseplayer(int i);//选择角色 void vs(int i,int j);//PK void Attack(int i,int j);//攻击 void recovery(int i,int j);//防御 void Energy(int i,int j);//能量 struct Kof { char name[20]; int Hp; int AP; int Dp; int Rp; }player[N]; void main() { srand(unsigned(time(NULL))); mainmenu(); } void login() { } void mainmenu() { while(1) { int x; printf("\n"); printf("\n"); printf(" ***************************************************************\n"); printf(" *** ***\n"); printf(" *** ***\n"); printf(" *** 欢迎进入Kof ***\n"); printf(" *** ***\n"); printf(" *** ***\n"); printf(" ***************************************************************\n"); printf("\t\t\t\t\t1----请先注册\n"); printf("\t\t\t\t\t2----查看玩家信息\n"); printf("\t\t\t\t\t3----查看自己信息\n"); printf("\t\t\t\t\t4----选择对手\n"); printf("\t\t\t\t\t5----结束\n"); printf("\n\n请选择菜单:"); scanf("%d",&x); if(x==5) break; switch(x) { case 1:Register();break; case 2:Showallplayer();break; case 3:Showplayer();break; case 4:Chooseopponent();break; } printf("\n\n\n\n\n按任意键继续:"); getch(); system("cls"); } } void Register() { char name[20]; printf("输入名称:"); fflush(stdin); gets(name); if(Checktoseek(name)==-1) { strcpy(player[n].name,name); player[n].Hp=rand()%1000; player[n].AP=rand()%(100+1)+50; player[n].Dp=rand()%(10-5+2)+5; player[n].Rp=rand()%(60+2)+60; n++; printf("人物创建成功\n"); } else { Register(); } } void
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页