iPhone 到 Windows Phone 7 – 动画和游戏





5.00/5 (3投票s)
在本教程的第二部分,我们将介绍如何连接 MainPage.xaml.cs 文件,并将“打鸟”游戏的所有对象整合到一个运行的应用程序中。
本教程分为两部分,收录于 《iPhone 开发者的 Windows Phone 7 编程指南》 和 《Silverlight 程序员的 Windows Phone 7 开发》。两篇内容同样适用,因此两个系列暂时合并。
在本教程的第二部分,我们将介绍如何连接 MainPage.xaml.cs 文件,并将“打鸟”游戏的所有对象整合到一个运行的应用程序中。
回顾一下:游戏名为“打鸟”,其目的是通过点击屏幕来“射击”屏幕上飞过的鸟(本游戏在创建过程中没有伤害任何活体鸟类)。每当屏幕上出现一只或多只鸟时,玩家有 3 次射击机会,在规定时间内击中目标。如果击中十只鸟,则进入下一关。随着关卡的推进,鸟的飞行速度会加快,射击难度也会增加。当玩家错过 10 只鸟时,游戏结束。当用户打开游戏时,会呈现简单、中等或困难的游戏选项。
- 简单模式游戏有一只鸟。
- 中等模式游戏有两只鸟,并且有一棵树会阻碍你的视线。
- 困难模式游戏有两棵树,并且鸟的飞行速度更快。
请记住,MainPage 控件是包含主要游戏控件的地方。在 LayoutRoot Canvas 中,有一系列空的 Canvas 对象,用于容纳计分控件、鸭子、背景元素和图标。这些 Canvas 将会写入对象,并用于维护不同游戏元素的期望的 Z 顺序。MainPage 对象还包含 startPage(如图 1 所示),以及一些名为 readyCanvas、niceShootingCanvas 和 flyAwayCanvas 的 Grid 对象。这些 Grid 包含简单的矩形,上面有“准备!”和“射得好!”等游戏消息。最后,有一个 gameOverCanvas,其中包含简单的游戏结束消息以及一个“再玩一次”按钮。
我们将使用 XNA 来播放一些音效,因此请将以下内容添加到您的 using 语句中:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
我们首先创建游戏中使用的每个对象的实例。在 MainPage()
构造函数之前,添加以下代码:
private readonly BackgroundElements _backgroundElements = new BackgroundElements();
private readonly duckIcons _duckIcons = new duckIcons();
private readonly SoundEffect _fieldSound;
private readonly SoundEffect _kickAss;
private readonly levelControl _levelControl = new levelControl();
private readonly ScoringControl _scoringControl = new ScoringControl();
private readonly ShotCounter _shotCounter = new ShotCounter();
除了随机数生成器,我们还将设置难度级别(“简单”、“中等”或“困难”)、击中鸭子数量、鸭子扇动翅膀的速度、是否已初始化困难关卡、一些最小和最大 X 和 Y 速度设置、生成的靶子数量、屏幕上的靶子数量、击中的靶子总数以及错过的靶子总数等成员变量。
private readonly Random _rand = new Random();
private string _difficulty;
private int _ducksHit;
private TimeSpan _flapSpeed;
private bool _hardLevelInitialized;
private int _maxXVel;
private int _maxYVel;
private int _minXVel;
private int _minYVel;
private int _numTargets;
private Duck[] _targets;
private int _targetsOnScreen;
private int _totalTargetsHit;
private int _totalTargetsMissed;
构造函数
从构造函数中开始编码,将 _backgroundElements
实例添加到 backgroundElements
Canvas 对象。接着是 _shotCounter
、_duckIcons
、_scoringControl
和 _levelControl
对象。
backgroundElements.Children.Add(_backgroundElements);
Canvas.SetLeft(_shotCounter, 10);
iconsCanvas.Children.Add(_shotCounter);
Canvas.SetLeft(_duckIcons, 155);
iconsCanvas.Children.Add(_duckIcons);
_duckIcons.initDuckIcons();
Canvas.SetLeft(_scoringControl, 600);
Canvas.SetTop(_scoringControl, 10);
scoringCanvas.Children.Add(_scoringControl);
Canvas.SetLeft(_levelControl, 10);
Canvas.SetTop(_levelControl, 10);
scoringCanvas.Children.Add(_levelControl);
创建事件处理器,用于游戏循环 storyboard、游戏开始时显示的难度按钮、duckCanvas 的 MouseLeftButtonDown
事件以及 Loaded 事件。
gameLoop.Completed += GameLoopCompleted;
btnEasy.Click += BtnEasyClick;
btnMedium.Click += BtnMediumClick;
btnHard.Click += BtnHardClick;
duckCanvas.MouseLeftButtonDown += DuckCanvasMouseLeftButtonDown;
_levelControl.Level = 1;
Loaded += MainPage_Loaded;
instructionsShow.Click += InstructionsShowClick;
instructionsClose.Click += InstructionsCloseClick;
playAgain.Click += PlayAgainClick;
levelMessageTimer.Completed += LevelMessageTimerCompleted;
audioTimer.Completed += AudioTimerCompleted;
levelTimer.Completed += LevelTimerCompleted;
readyMessage.Completed += ReadyMessageCompleted;
closeInstructionsStoryboard.Completed += CloseInstructionsStoryboardCompleted;
_kickAss = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/out_of_gum.wav"));
_fieldSound = SoundEffect.FromStream(TitleContainer.OpenStream(
"sounds/fieldAmbientSounds.wav"));
方法
当 Silverlight 应用程序加载完成时(惊喜!),Loaded 事件处理程序就会被触发。
此事件仅用于一个目的——提高 WP7 模拟器的性能。虽然效果因人而异,但在某些情况下,启用帧率计数器可以使游戏运行更流畅。因此,如果您遇到卡顿的动画和普遍较差的性能,请尝试启用帧率计数器。如果您的笔记本电脑性能良好,则可以省略以下方法。
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.Host.Settings.EnableFrameRateCounter = true;
}
简单、中等和困难按钮的事件处理程序非常相似。 这三个方法都以将 _backgroundElements 对象设置为相应级别开始——回想一下,我们有树木来遮挡视线,可以根据级别启用。
_min 和 _max 速度变量用于设置靶子在屏幕上移动的最小和最大速度,而 _numTargets 用于确定关卡开始时屏幕上将生成多少个靶子。
接下来是一个字符串,用于引用所选的级别,以及我们想要为每个击中的靶子指定的点数。_flapSpeed
变量是一个 TimeSpan 对象,用于确定鸭子扇动翅膀的速度。它们在屏幕上移动的速度越快,扇动翅膀的速度也应该越快。levelTimer 的 Duration 属性确定玩家在靶子飞走之前有多少时间来射击屏幕上的靶子。最后,我们调用几个方法来清除和重置我们的游戏屏幕。
private void BtnEasyClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetEasy();
_minXVel = 1;
_maxXVel = 1;
_minYVel = 1;
_maxYVel = 1;
_numTargets = 1;
_difficulty = "easy";
_scoringControl.PointsForEachDuck = 100;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 250);
levelTimer.Duration = new TimeSpan(0, 0, 15);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
private void BtnMediumClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetMedium();
_minXVel = 2;
_maxXVel = 4;
_minYVel = 2;
_maxYVel = 4;
_numTargets = 2;
_difficulty = "medium";
_scoringControl.PointsForEachDuck = 500;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 175);
levelTimer.Duration = new TimeSpan(0, 0, 10);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
private void BtnHardClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetHard();
_minXVel = 4;
_maxXVel = 6;
_minYVel = 4;
_maxYVel = 6;
_numTargets = 2;
_difficulty = "hard";
_scoringControl.PointsForEachDuck = 2500;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 100);
levelTimer.Duration = new TimeSpan(0, 0, 7);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
_duckIcons.initDuckIcons()
和 _duckIcons.resetDuckIcons()
方法已经存在,因为我们的项目中已经有了 _duckIcons
对象。但是,ResetScoreAndShots()
和 InitObjects()
方法则没有。
ResetScoreAndShots()
方法相当直接。它重置发射的弹药数、当前存储的分数以及 _scoringControl
对象中显示的分数。
InitObjects()
方法更复杂。此方法用于设置我们的对象以供游戏使用。此方法首先将 Duck 对象数组 _targets
初始化为由 _numTargets
决定的长度,然后使用 for 循环生成新的 Duck 对象。回想一下,_numTargets
是在级别按钮的单击事件中设置的,
private void ResetScoreAndShots()
{
_shotCounter.ShotsFired = 0;
_scoringControl.Score = 0;
_scoringControl.SetScore("0000000")
}
private void InitObjects()
{
_targets = new Duck[_numTargets];
for (var i = 0; i < _numTargets; i++)
{
_targets[i] = new Duck();
}
}
迭代目标数组并分配 _flapSpeed,并使用我们的随机数生成器来确定目标的最小和最大 X 和 Y 速度。之后,随机化对象的起始 X 和 Y 位置。
StartX 是 0 到 800 之间的随机数,但我们希望鸭子一开始就处于屏幕外,所以我们在这里进行一个快速检查——如果 StartX 小于 400,我们将鸭子设置在屏幕左侧,位置设为 0 – 鸭子对象的宽度。否则,我们就知道它从屏幕右侧开始,所以我们将 X 缩放翻转以让鸭子掉头,将鸭子的位置设置在屏幕外右侧,并确保 X 速度设置为负数,使鸭子向左飞行。
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].duckFly.Duration = _flapSpeed;
_targets[i].VelY = _rand.Next(_minYVel, _maxYVel);
_targets[i].VelX = _rand.Next(_minXVel, _maxXVel);
_targets[i].StartY = _rand.Next(Convert.ToInt16(LayoutRoot.Height - 200));
_targets[i].StartX = _rand.Next(800);
if (_targets[i].StartX <= 400)
{
_targets[i].StartX = 0 - Convert.ToInt16(_targets[i].Width);
}
else
{
_targets[i].duckScale.ScaleX *= -1;
_targets[i].StartX = 801;
_targets[i].VelX *= -1;
}
为鸭子对象上的碰撞区域设置事件处理器(我们稍后将编写此方法),并根据我们刚刚生成的值将鸭子定位在屏幕上。然后处理鸭子的叫声分配。我们不希望每只鸭子都有相同的音效,所以第一个分配一种声音,第二个分配另一种声音。我们知道目标的最大数量是 2。如果这个数字增加,就需要重构此代码来更改每只鸭子的声音,
_targets[i].MouseLeftButtonDown += HitZoneMouseLeftButtonDown;
Canvas.SetLeft(_targets[i], _targets[i].StartX);
Canvas.SetTop(_targets[i], _targets[i].StartY);
if (i == 0)
{
_targets[i].SetQuack("sounds/duck01.wav", 5);
}
else
{
_targets[i].SetQuack("sounds/duck02.wav", 2);
}
_targetsOnScreen += 1;
duckCanvas.Children.Add(_targets[i]);
_targets[i].duckFly.Begin();
}
startPage.Visibility = Visibility.Collapsed;
readyCanvas.Visibility = Visibility.Visible;
进行的最后检查是查看玩家是否选择了困难模式。如果是,我们通过播放一个音频片段来初始化困难模式。之所以这样处理,是因为音频片段只应播放一次,而不是在困难模式下每次释放屏幕上的目标时都播放。在这种情况下,audioTimer 会延迟游戏 4 秒钟,这是音频播放所需的时间。如果不是困难模式,我们可以启动 readyMessage
计时器,它将把我们带入游戏循环。
if (_difficulty == "hard" && !_hardLevelInitialized)
{
FrameworkDispatcher.Update();
_kickAss.Play();
_hardLevelInitialized = true;
audioTimer.Duration = new TimeSpan(0, 0, 4);
audioTimer.Begin();
}
else
{
readyMessage.Begin();
}
}
在前面的方法中,我们设置了一个事件处理器来确定目标是否被击中。 该方法首先检查射击计数器是否处于重新加载模式。如果霰弹枪正在重新加载,则无法射击目标。如果枪支没有重新加载,我们将增加 _ducksHit
变量,调用 _scoringControl
对象来更新我们的分数,调用 _duckIcons
对象来更新我们的 duckIcon
显示,并增加我们的 _totalTargetsHit
变量。
之后,我们需要确定击中了哪个目标,所以我们使用 sender 来捕获它。如果目标对象不为空,我们将调用该对象的 DuckShot()
方法,该方法正如您所回忆的,会将鸭子送入死亡螺旋。通过调用 CheckShotLimit()
方法来完成此方法。
private void HitZoneMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!_shotCounter.Reloading)
{
_ducksHit += 1;
_scoringControl.UpdateScore(_levelControl.Level);
_duckIcons.hitIcon();
_totalTargetsHit += 1;
var whichDuckIsHit = sender as Duck;
if (whichDuckIsHit != null) whichDuckIsHit.DuckShot();
CheckShotLimit();
}
}
CheckShotLimit()
方法会跟踪已发射的弹药数与已击中目标的数量。如果发射的弹药数为 3,并且玩家尚未击中屏幕上的所有目标,则游戏会停止关卡计时器,显示 flyAwayCanvas(“飞走!”),将每只鸭子设置为飞走模式,并增加 _totalTargetsMissed
变量。
如果屏幕上的所有鸭子都被击中,则停止关卡计时器,并显示“射得好!”消息。
private void CheckShotLimit()
{
if (_shotCounter.ShotsFired == 3 && _ducksHit < _numTargets)
{
levelTimer.Stop();
for (var i = 0; i < _targets.Length; i++)
{
if (!_targets[i].IsShot)
{
flyAwayCanvas.Visibility = Visibility.Visible;
_targets[i].FlyAway = true;
_totalTargetsMissed += 1;
}
}
}
if (_ducksHit == _targets.Length)
{
levelTimer.Stop();
niceShootingCanvas.Visibility = Visibility.Visible;
}
}
此时,许多游戏逻辑已经到位,但我们从构造函数中剩余的事件处理器那里走开了,所以让我们通过编写 GameLoopCompleted
方法来回到它们。当 gameLoop 计时器到期时,将调用此方法。因为 gameLoop 计时器是一个空的 storyboard,所以它在启动时就会到期,从而触发 Completed 事件。通过重新启动计时器,在游戏中创建了一个正在运行的循环。
此方法的第一部分会物理地移动屏幕上的鸭子。请记住,鸭子有两个动画在工作——一个让它们的翅膀扇动,然后是使它们在屏幕上移动的游戏循环。这是通过为每只鸭子调用 MoveDuck()
方法来完成的。如果一只鸭子被击中并处于死亡螺旋状态,则会检查该鸭子是否已掉落屏幕外,如果是,则将其移除。对象的移除由 RemoveObjects()
方法处理。
如果屏幕上没有目标剩余,则停止游戏循环,并调用 DoLevel() 方法(我们稍后也会对其进行编码)。否则,我们就知道目标仍然在屏幕上并且是活动的,所以我们可以重新启动 gameLoop 计时器。
当需要从游戏中移除鸭子时(因为它已被击中并掉落到屏幕外,或者因为它被允许飞走),就会调用 RemoveObjects()
方法。请注意,此方法会传递一个整数,该整数代表对象在 _targets 数组中的位置。
此方法以一个 if 语句开始,以查看对象是否已被击中。如果没有,则调用 _duckIcons 对象的 missIcon()
方法。之后,会检查玩家是否总共错过了 10 只或更多的鸭子。如果是,则停止消息计时器,并调用 GameOverMan()
方法来结束游戏。否则,游戏可以继续进行,但通过调用鸭子对象上的 Remove()
duck 方法,选定的目标将从游戏中移除,并且 _targetsOnScreen
变量会被递减。 当屏幕上的目标清除(通过被击中或飞走)时,就会调用 DoLevel()
方法。此方法首先清除 duckCanvas 的子对象。接下来,停止 levelTimer 和 gameLoop,并隐藏 flyAwayCanvas。_ducksHit
变量会重置,_shotCounter
对象也会重置。
private void GameLoopCompleted(object sender, EventArgs e)
{
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].MoveDuck();
if (Canvas.GetTop(_targets[i]) >=
duckCanvas.Height && _targets[i].IsShot)
{
RemoveObjects(i);
}
if (_targets[i].FlyAway)
{
if (Canvas.GetLeft(_targets[i]) <=
-_targets[i].ActualWidth)
{
RemoveObjects(i);
}
if (Canvas.GetLeft(_targets[i]) >
duckCanvas.Width)
{
RemoveObjects(i);
}
}
}
if (_targetsOnScreen == 0)
{
gameLoop.Stop();
DoLevel();
}
else
{
gameLoop.Begin();
}
}
private void RemoveObjects(int i)
{
if (!_targets[i].IsShot)
{
_duckIcons.missIcon();
if (_duckIcons.missedCounter >= 10)
{
levelMessageTimer.Stop();
GameOverMan();
}
}
_targets[i].RemoveDuck();
_targetsOnScreen -= 1;
}
private void DoLevel()
{
duckCanvas.Children.Clear();
levelTimer.Stop();
gameLoop.Stop();
flyAwayCanvas.Visibility = Visibility.Collapsed;
_ducksHit = 0;
_shotCounter.ResetShots(0);
}
以下代码会在玩家通过 10 个目标后稍微提高游戏速度。无论目标是否全部击中,游戏都会加速(加速? 提升?)。 _totalTargetsHit
变量会重置以准备下一组目标,并且 _levelControl
对象会被递增。图标计数器会重置,图标的外观和感觉也会重置。
if (_duckIcons.iconCounter == 10)
{
_minXVel += 1;
_maxXVel += 1;
_minYVel += 1;
_maxYVel += 1;
_totalTargetsHit = 0;
_levelControl.Level += 1;
_duckIcons.iconCounter = 0;
_duckIcons.resetDuckIcons();
}
该方法最后进行一些收尾工作。_levelControl
会更新以显示当前级别,并折叠任何可能可见的消息 Canvas 对象。“准备!”消息会显示,并且 levelMessageTimer
会启动,以让玩家为级别做好准备。
_levelControl.SetMessageLevel();
flyAwayCanvas.Visibility = Visibility.Collapsed;
niceShootingCanvas.Visibility = Visibility.Collapsed;
readyCanvas.Visibility = Visibility.Visible;
levelMessageTimer.Begin();
当游戏结束时,会调用 GameOverMan()
方法。此方法会清除 duckCanvas,停止消息计时器,并重置 _targetsOnScreen
和 _hardLevelInitialized
变量。然后它会隐藏消息 Canvas,然后显示 gameOverCanvas
和“再玩一次”按钮。
private void GameOverMan()
{
duckCanvas.Children.Clear();
readyMessage.Stop();
flyAwayMessage.Stop();
levelMessageTimer.Stop();
levelTimer.Stop();
_targetsOnScreen = 0;
_hardLevelInitialized = false;
readyCanvas.Visibility = Visibility.Collapsed;
niceShootingCanvas.Visibility = Visibility.Collapsed;
flyAwayCanvas.Visibility = Visibility.Collapsed;
gameOverCanvas.Visibility = Visibility.Visible;
playAgain.Visibility = Visibility.Visible;
}
好的,回到连接事件!接下来,我们有 DuckCanvasMouseLeftbuttonDown
方法。此方法很简单——它检查霰弹枪是否正在重新加载。如果不是,则调用 _shotCounter
对象的 FireShot()
方法,然后调用我们刚才编写的 CheckShotLimit()
方法。
接下来是处理说明的方法。当主屏幕上的“说明”链接被选中时,会调用 InstructionsShowClick
。此方法会使包含说明的两个控件可见,然后播放 openInstructionsStoryboard
。instructionsCloseClick
方法播放 closeInstructionsStoryboard
,并且当说明隐藏时触发的 CloseInstructionsStoryboardCompleted
事件会将可见性设置回 Collapsed。
当玩家在游戏结束后选择“再玩一次”按钮时,会调用 PlayAgainClick
方法。此方法的作用是重置游戏中的所有对象到默认状态,以便游戏可以再次开始。
private void DuckCanvasMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!_shotCounter.Reloading)
{
_shotCounter.FireShot(_ducksHit, _difficulty);
CheckShotLimit();
}
}
private void InstructionsShowClick(object sender, RoutedEventArgs e)
{
Instructions.Visibility = Visibility.Visible;
stackPanelInstructions.Visibility = Visibility.Visible;
openInstructionsStoryboard.Begin();
}
private void InstructionsCloseClick(object sender, RoutedEventArgs e)
{
closeInstructionsStoryboard.Begin();
}
private void CloseInstructionsStoryboardCompleted(object sender, EventArgs e)
{
Instructions.Visibility = Visibility.Collapsed;
}
private void PlayAgainClick(object sender, RoutedEventArgs e)
{
gameLoop.Stop();
startPage.Visibility = Visibility.Visible;
gameOverCanvas.Visibility = Visibility.Collapsed;
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].RemoveDuck();
duckCanvas.Children.Remove(_targets[i]);
}
_difficulty = "";
_levelControl.Level = 1;
_shotCounter.ShotsFired = 0;
_ducksHit = 0;
_totalTargetsHit = 0;
_totalTargetsMissed = 0;
_targetsOnScreen = 0;
_duckIcons.initDuckIcons();
_scoringControl.Score = 0;
_scoringControl.PointsForEachDuck = 0;
_scoringControl.SetScore("0000000");
}
最后,我们需要为游戏中的消息以及控制玩家射击时间的 levelTimer 的各种 Completed 事件创建事件处理器。当玩家耗尽射击时间时,会调用 LevelTimerCompleted
方法。此方法会显示“飞走!”消息,并将 FlyAway 标志设置为所有目标的 true,同时增加 _totalTargetsMissed
变量。
在游戏屏幕准备好之后,ReadyMessageCompleted
方法会执行,并提示玩家关卡即将开始(“准备!”)。此方法会隐藏消息,并启动控制玩家射击时间的计时器。它会启动背景环境声音,并为关卡中的每个目标启动叫声(Quack)音效。最后,启动 gameLoop 以开始游戏。
private void LevelMessageTimerCompleted(object sender, EventArgs e)
{
niceShootingCanvas.Visibility = Visibility.Collapsed;
InitObjects();
}
private void AudioTimerCompleted(object sender, EventArgs e)
{
readyMessage.Begin();
}
private void LevelTimerCompleted(object sender, EventArgs e)
{
for (var i = 0; i < _targets.Length; i++)
{
if (!_targets[i].IsShot)
{
flyAwayCanvas.Visibility = Visibility.Visible;
_targets[i].FlyAway = true;
_totalTargetsMissed += 1;
}
}
private void ReadyMessageCompleted(object sender, EventArgs e)
{
readyCanvas.Visibility = Visibility.Collapsed;
levelTimer.Begin();
FrameworkDispatcher.Update();
_fieldSound.Play();
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].Quack();
}
gameLoop.Begin();
}