用WPF做2D游戏
引言
WPF使用了DirectX作为图形渲染引擎,因此游戏性能的表现要强于GDI+,而且WPF内建的对动画的支持使得游戏的编写更加简化。
WPF也提供3D图形的功能,不过3D的建模和动画比较复杂,这里先做一个2D的游戏引擎练练手。
实例
一直想做一个超级马里奥的游戏,就从这个游戏做起,画了一部分图,已经完成的有走动、跳跃、发射子弹、边界检查和场景滚动,没有关卡,没有敌人。
下面是游戏截图:
实现
行走中迈腿摆臂的动画是通过切换图片帧来实现的,切换帧有两种方法,一种是放一个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>
<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;
}
}
{
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>
<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;
}
}
{
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>
<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();
}
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;
}
}
}
{
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矢量图来做动画,场景物体等也全都用矢量图,这样的好处一是可以任意放大缩小,二是动画效果会更加流畅一些。