65.9K
CodeProject 正在变化。 阅读更多。
Home

iPhone 到 Windows Phone 7 – 动画和游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2010年12月14日

CPOL

12分钟阅读

viewsIcon

22264

在本教程的第二部分,我们将介绍如何连接 MainPage.xaml.cs 文件,并将“打鸟”游戏的所有对象整合到一个运行的应用程序中。

原文发布于 Jesse Liberty 的博客

本教程分为两部分,收录于 《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。此方法会使包含说明的两个控件可见,然后播放 openInstructionsStoryboardinstructionsCloseClick 方法播放 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();
}
© . All rights reserved.