用WPF做2D游戏

引言

WPF使用了DirectX作为图形渲染引擎,因此游戏性能的表现要强于GDI+,而且WPF内建的对动画的支持使得游戏的编写更加简化。

WPF也提供3D图形的功能,不过3D的建模和动画比较复杂,这里先做一个2D的游戏引擎练练手。

实例

一直想做一个超级马里奥的游戏,就从这个游戏做起,画了一部分图,已经完成的有走动、跳跃、发射子弹、边界检查和场景滚动,没有关卡,没有敌人。

下面是游戏截图:

实现

mario.png

行走中迈腿摆臂的动画是通过切换图片帧来实现的,切换帧有两种方法,一种是放一个Image,然后用ObjectAnimationUsingKeyFrames来改变Image的Source属性:

XAML代码
        <Storyboard x:Key="walkLeftStoryboard">
            
<ObjectAnimationUsingKeyFrames Duration="00:00:00.4" RepeatBehavior="Forever"
                                           Storyboard.TargetName
="marioImage" Storyboard.TargetProperty="Source" />
        
</Storyboard>

        
<Image Name="marioImage">
            
<Image.RenderTransform>
                
<TranslateTransform x:Name="marioTranslate"  X="0" Y="0"/>
            
</Image.RenderTransform>
        
</Image>

然后用C#代码添加帧: 

代码
        public static System.Drawing.Bitmap LoadBitmap(Uri uri)
        {
            StreamResourceInfo info 
= Application.GetResourceStream(uri);
            
return new System.Drawing.Bitmap(info.Stream);
        }

        
public static BitmapSource CreateBitmapSource(System.Drawing.Bitmap frame)
        {
            BitmapSource bs 
= System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
              frame.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            
return bs;
        }

        
internal static void AddKeyFrames(ObjectAnimationUsingKeyFrames animation, params System.Drawing.Bitmap[] frames)
        {
            
double percent = 0;
            
double pace = 1.0 / frames.Length;
            
foreach (var frame in frames)
            {
                BitmapSource bs 
= CreateBitmapSource(frame);
                animation.KeyFrames.Add(
new DiscreteObjectKeyFrame(bs, KeyTime.FromPercent(percent)));
                percent 
+= pace;
            }
        }

使用这种方法需要先把大图在代码里切割成四个小图,由于要用到System.Drawing所以代码不能迁移到Silverlight。

于是我又想了第二种方法,用ImageBrush做控件背景,然后用ObjectAnimationUsingKeyFrames来切换它的ViewBox。  

代码
        <Storyboard x:Key="walkLeftStoryboard">
            
<ObjectAnimationUsingKeyFrames Duration="00:00:00.4" RepeatBehavior="Forever"
                                           Storyboard.TargetName
="marioImage" Storyboard.TargetProperty="CurrentFrame" />
        
</Storyboard>

        
<game:AnimatedImage x:Name="marioImage" Image="/SuperMario;component/Images/mario.png" CurrentFrame="0, 0, 0.5, 0.5" Width="134" Height="131">
            
<game:AnimatedImage.RenderTransform>
                
<TranslateTransform x:Name="marioTranslate"  X="0" Y="0"/>
            
</game:AnimatedImage.RenderTransform>
        
</game:AnimatedImage>

用C#代码添加帧:

代码
        internal static void AddKeyFrames(ObjectAnimationUsingKeyFrames animation, Rect[] frames)
        {
            
double percent = 0;
            
double pace = 1.0 / frames.Length;
            
foreach (var frame in frames)
            {
                animation.KeyFrames.Add(
new DiscreteObjectKeyFrame(frame, KeyTime.FromPercent(percent)));
                percent 
+= pace;
            }
        }

 AnimatedImage是一个自定义的控件,控件模板如下: 

    <Style TargetType="local:AnimatedImage">
        
<Setter Property="Template">
            
<Setter.Value>
                
<ControlTemplate TargetType="local:AnimatedImage">
                    
<Border BorderThickness="0">
                        
<Border.Background>
                            
<ImageBrush ImageSource="{Binding Path=Image,RelativeSource={RelativeSource TemplatedParent}}"
                                        Stretch
="UniformToFill" AlignmentX="Left" AlignmentY="Top" Viewbox="{Binding Path=CurrentFrame,RelativeSource={RelativeSource TemplatedParent}}"/>
                        
</Border.Background>
                    
</Border>
                
</ControlTemplate>
            
</Setter.Value>
        
</Setter>
    
</Style>

后来发现SliverLight里的TileBrush没有ViewBox属性,所以还是无法迁移

接下来就是在GameLoop中根据键盘按键控制动画的开始和停止,并用marioTranslate来改变人物的位置。

GameLoop可由CompositionTarget.Rendering事件指定:

        GameLoop gameLoop;
        
private void Window_Loaded(object sender, RoutedEventArgs e)
        {       
            ......
            gameLoop 
= new GameLoop(player, Scenes.Level1);
            CompositionTarget.Rendering 
+= new EventHandler(CompositionTarget_Rendering);
        }

        
void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            gameLoop.ProcessChanges();
        }

  

在GameLoop中还需要注意的就是跳跃过程中重力效果的模拟和对物体、台阶、边界的碰撞检查,这个就不多说了,看代码:

代码
        public void ProcessChanges()
        {
            TimeSpan timeSpan 
= DateTime.Now - lastTime;
            
double step = timeSpan.TotalSeconds;
            lastTime 
= DateTime.Now;

            
double x = Sprite.X;
            
double y = Sprite.Y;
            
double dx = step * Sprite.Speed;
            
if (Sprite.IsWalkingLeft)
            {
                x 
-= dx;
                Scene.ScrollRightt(x, dx);
            }
            
else if (Sprite.IsWalkingRight)
            {
                x 
+= dx;
                Scene.ScrollLeft(x, dx);
            }
            
if (Map.CanMoveTo(x, Sprite.Y, Sprite.Width, Sprite.Height))
            {
                Sprite.X 
= x;
            }

            
if (Sprite.IsJumping)
            {
                y 
-= (1 - Sprite.JumpTime) * step * 400;
                
if (Sprite.JumpTime < 1 && Map.CanMoveTo(Sprite.X, y, Sprite.Width, Sprite.Height))
                {
                    Sprite.Y 
= y;
                    Sprite.JumpTime 
+= step;
                }
                
else
                {
                    Sprite.IsJumping 
= false;
                    Sprite.IsFalling 
= true;
                    Sprite.JumpTime 
= 0;
                }
            }
            
else if (Sprite.IsFalling)
            {
                y 
+= 800 * Sprite.FallTime * step;
                
if (Map.CanMoveTo(Sprite.X, y, Sprite.Width, Sprite.Height))
                {
                    Sprite.Y 
= y;
                    Sprite.FallTime 
+= step;
                }
                
else
                {
                    Sprite.IsFalling 
= false;
                    Sprite.FallTime 
= 0;
                }
            }
            
else
            {
                y 
+= 1;
                
if (Map.CanMoveTo(Sprite.X, y, Sprite.Width, Sprite.Height))
                {
                    Sprite.Y 
= y;
                    Sprite.IsFalling 
= true;
                    Sprite.FallTime 
= step;
                }
            }
        }

下一步

下一步我打算用XAML矢量图来做动画,场景物体等也全都用矢量图,这样的好处一是可以任意放大缩小,二是动画效果会更加流畅一些。

 

原文地址:https://www.cnblogs.com/rufi/p/WPFSuperMarioGame.html