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

Snail Run for Windows Phone

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (23投票s)

2012年2月29日

CPOL

13分钟阅读

viewsIcon

59510

downloadIcon

1831

了解如何在同一应用程序中集成 Silverlight 和 XNA for Windows Phone

目录

引言

到目前为止,使用 Windows Phone 进行游戏开发非常有趣。在这篇文章中,我将介绍“蜗牛快跑!”这款游戏,这是一款传统的迷宫、吃豆人类型的游戏。除了有趣之外,这里真正值得关注的重点是:在同一个解决方案中结合 Silverlight 和 XNA 开发,使用“Mappy”地图创建工具,在 Visual Studio 游戏项目中导入/处理地图,以及如何实现寻路算法,特别是“A* 搜索算法”,为游戏注入活力。

系统要求

要使用本文提供的 Windows Phone 版蜗牛快跑,您必须从 Microsoft 直接下载并安装以下 100% 免费的开发工具:

  • Visual Studio 2010 Express for Windows Phone
    无论您是熟悉还是初次接触 Silverlight 和 XNA Game Studio 编程,Visual Studio 2010 Express for Windows Phone 都提供了您开始构建 Windows Phone 应用程序所需的一切。

  • Windows Phone SDK 7.1
    Windows Phone 软件开发工具包 (SDK) 7.1 为您提供了开发 Windows Phone 7.0 和 Windows Phone 7.5 设备上的应用程序和游戏所需的所有工具。
  • Silverlight 和 XNA:成为朋友

    Windows Phone 以为开发者提供两种不同的框架而闻名:Silverlight 和 XNA。虽然 Silverlight 适用于大多数应用程序需求,并且足够使用,因为它具有固有的页面导航、XAML 标记语言的灵活性、内置的转换、故事板和动画、高级数据绑定、矢量渲染、全景和枢轴应用程序,仅举几例,而另一方面,更新/绘图循环、视觉和音频效果以及 XNA Framework 的快速精灵渲染引擎通常是开发者在游戏开发方面的首选。

    如果您已经单独使用 XNA Framework 开发了 Windows Phone 游戏,您可能已经遇到并怀念缺乏页面导航、XAML 和绑定以及 Silverlight 开发的其他好处。一些琐碎的应用程序需求,例如渲染简单的列表框,在 XNA 中可能需要大量工作。幸运的是,一旦您决定创建一个新的 Visual Studio 项目,选择一个名为 **Windows Phone Silverlight and XNA Application** 的项目模板,这种架构上的困境就结束了。

    这个名为“MyGame”的项目解决方案模板创建了一个包含三个项目的新解决方案:

    • MyGame,这是一个 Silverlight 项目,但也包含对 XNA Framework 的引用。这是 Silverlight 和 XNA 一起渲染的地方。
    • MyGameLib,这是一个纯 XNA 项目。您可以使用此项目来重用现有的 XNA 代码或将 XNA 代码与 Silverlight 代码分开。
    • MyGameLibContent:这是内容管道项目,您可以在其中找到解决方案的资产。

    当您运行应用程序时,您会发现它看起来非常像一个普通的 Silverlight 应用程序。这是MainPage.xaml页面

    唯一显着的例外是中央按钮,它显然会导航到游戏页面。

        <!--Create a single button to navigate to the second page which is rendered with the XNA Framework-->
        <Button Height="100" Content="Change to game page" Click="Button_Click" />
    
            // Simple button Click event handler to take us to the second page
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                NavigationService.Navigate(new Uri("/GamePage.xaml", UriKind.Relative));
            }
    

    至于游戏页面:“GamePage.xaml”使其听起来也像一个常规的 Silverlight 页面,但这并非如此。GamePage是 Silverlight 和 XNA 真正混合在一起的地方。目前您会发现该页面的 XAML 是空的,所以之后我们需要在上面做一些工作。

    <phone:PhoneApplicationPage 
        x:Class="MyGame.GamePage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
        xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        FontFamily="{StaticResource PhoneFontFamilyNormal}"
        FontSize="{StaticResource PhoneFontSizeNormal}"
        Foreground="{StaticResource PhoneForegroundBrush}"
        SupportedOrientations="Portrait" Orientation="Portrait"
        mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480"
        shell:SystemTray.IsVisible="False">
        
        <!--No XAML content is required as the page is rendered entirely with the XNA Framework-->
    
    </phone:PhoneApplicationPage>
    

    现在让我们看一下背后的代码类GamePage页面

    首先,您会注意到该类中存在一些 XNA 元素:ContentManager, GameTimerSpriteBatch:

        public partial class GamePage : PhoneApplicationPage
        {
            ContentManager contentManager;
            GameTimer timer;
            SpriteBatch spriteBatch;
    
            public GamePage()
            {
                InitializeComponent();
    
                // Get the content manager from the application
                contentManager = (Application.Current as App).Content;
    
                // Create a timer for this page
                timer = new GameTimer();
                timer.UpdateInterval = TimeSpan.FromTicks(333333);
                timer.Update += OnUpdate;
                timer.Draw += OnDraw;
            }
    

    然后您有OnNavigatedTo类,其中SpriteBatch被实例化,GameTimer已启动,XNA 渲染已开启。此外,这里也是加载游戏内容(精灵、声音等)的地方。

            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                // Set the sharing mode of the graphics device to turn on XNA rendering
                SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
    
                // Create a new SpriteBatch, which can be used to draw textures.
                spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
    
                // TODO: use this.content to load your game content here
    
                // Start the timer
                timer.Start();
    
                base.OnNavigatedTo(e);
            }
    

    每当您导航离开GamePage时,XNA 渲染就会被禁用,并且计时器会停止。

            protected override void OnNavigatedFrom(NavigationEventArgs e)
            {
                // Stop the timer
                timer.Stop();
    
                // Set the sharing mode of the graphics device to turn off XNA rendering
                SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
    
                base.OnNavigatedFrom(e);
            }
    

    如果您已经是 XNA 开发者,您会注意到OnUpdate方法是熟悉 XNA 的更新方法的替代。在这里,您可以运行逻辑,例如更新世界、检查碰撞、收集输入和播放音频。

            private void OnUpdate(object sender, GameTimerEventArgs e)
            {
                // TODO: Add your update logic here
            }
    

    OnUpdate方法一起,OnDraw也是熟悉 XNA 的更新方法的对应项。如您所见,这里没有太多内容,但这就是所有游戏渲染将发生的地方。

            private void OnDraw(object sender, GameTimerEventArgs e)
            {
                SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
    
                // TODO: Add your drawing code here
            }
    

    从上面的代码可以看出,默认的 **Silverlight and XNA** 模板足以节省您一些时间和精力,但不幸的是,它留下了一些重要的部分没有解释,尽管代码中散布着注释。在文章的下一部分,我们将尝试处理这些空白。

    重构 MainPage.xaml

    第一次化妆是在MainPage页面上完成的。如前所述,这个页面是一个普通的 Silverlight 页面。它是我们应用程序的入口点,因此在这里放置用户选项的链接会很有趣,例如设置、排行榜、关于页面以及最明显的,游戏开始。我没有改变任何功能,只是创建了一个前景图像,并对背景中出现的泡沫应用了一些动画,以营造游戏的潜艇氛围。

    这里有趣的点在于泡沫动画的制作。本身就有一个实例MainPageSplashScreen控件,而该控件又负责动画。代码背后的类创建了正好 10 个

            <!--ContentPanel - place additional content here-->
            <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
                <ctr:SplashScreen x:Name="splashScreen" VerticalAlignment="Center" Grid.ColumnSpan="3" Grid.RowSpan="3"/>
                <Image Source="MainPage.png" MouseLeftButtonUp="Image_MouseLeftButtonUp"></Image>
            </Grid>
    

    控件,而该控件又负责动画。元素,每个元素都经过独立动画的处理。每次动画都会导致相应的气泡从椭圆底部快速移动到顶部。此外,一个画布EasingFunction被应用于动画,使得移动看起来更自然。整个动画过程超出了本文的范围,但这里是代码,以防您想研究它是如何工作的。本节介绍了如何让 Silverlight 和 XNA 一起工作。我花了一些时间才弄清楚如何做到这一点。

            private void CreateBubbles()
            {
                var linearBubbleBrush = new LinearGradientBrush() 
                { StartPoint = new Point(1, 0), EndPoint = new Point(0, 1) };
                linearBubbleBrush.GradientStops.Add(
                NewGradient.Stop(Color.FromArgb(0xFF, 0x00, 0x20, 0x40), 0.0));
                linearBubbleBrush.GradientStops.Add(
                NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
    
                var radialBubbleBrush = new RadialGradientBrush() {
                 Center = new Point(0.25, 0.75), RadiusX = .3, RadiusY = .2, GradientOrigin = new Point(0.35, 0.75) };
                radialBubbleBrush.GradientStops.Add(
                NewGradient.Stop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0));
                radialBubbleBrush.GradientStops.Add(
                NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
    
                for (var i = 0; i < 10; i++)
                {
                    var diameter = 10 + (i % 4) * 10;
                    var ms = 2000 + i % 7 * 500;
    
                    var ellBubble = new Ellipse()
                    {
                        Width = diameter,
                        Height = diameter,
                        Stroke = linearBubbleBrush,
                        Fill = radialBubbleBrush,
                        StrokeThickness = 3
                    };
    
                    ellBubble.SetValue(Canvas.LeftProperty,
                     i * (40.0 + 40.0 - diameter / 2));
                    ellBubble.SetValue(Canvas.TopProperty, 0.0 + 40.0 - diameter / 2);
    
                    cnvBubbles.Children.Add(ellBubble);
    
                    var leftAnimation = new DoubleAnimation()
                    {
                        From = 40.0 * i,
                        To = 40.0 * i,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    var topAnimation = new DoubleAnimation()
                    {
                        From = 400,
                        To = 0,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    var opacityAnimation = new DoubleAnimation()
                    {
                        From = 1.0,
                        To = 0.0,
                        Duration = TimeSpan.FromMilliseconds(ms)
                    };
                    Storyboard.SetTarget(leftAnimation, ellBubble);
                    Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
                    Storyboard.SetTarget(topAnimation, ellBubble);
                    Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
                    Storyboard.SetTarget(opacityAnimation, ellBubble);
                    Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
                    leftAnimation.EasingFunction = new BackEase() 
                    { Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
                    topAnimation.EasingFunction = new BackEase() 
                    { Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
    
                    var sb = new Storyboard();
                    sb.Children.Add(leftAnimation);
                    sb.Children.Add(topAnimation);
                    sb.Children.Add(opacityAnimation);
                    sb.RepeatBehavior = RepeatBehavior.Forever;
    
                    bubbles.Add(ellBubble);
                    storyBoards.Add(sb);
    
                    sb.Begin();
                }
            }
    

    GamePage:让 Silverlight 和 XNA 工作起来

    首先,您必须向项目中添加内容。我们拥有的内容类型只有精灵和关卡地图。**精灵 (Sprite)** 是一个常见的概念,不需要进一步解释。我们有蜗牛、珍珠、鱿鱼和海星的图形内容。这些是我们游戏中的角色。**关卡地图 (Level maps)** 是由第三方开源工具 **MapWin** 创建的地图,然后被整合到我们的游戏内容中。

    现在,让我们看一下位于 **GamePage.xaml.cs** 的代码背后的类。该类作用域内值得注意的元素是:

    已知的

    • 实例。ContentManager, GameTimerSpriteBatchCamera2d 实例。顾名思义,它用于在我们穿越游戏迷宫时进行滚动/缩放。
    • UIElementRenderer 是最重要的部分:它将允许我们将 Silverlight 内容渲染到纹理中,从而我们可以将它们与 XNA 生成的图形放在一起。
    • GameSettings 用于预定义的遊戲設定,例如速度、屏幕分辨率等。
    • ScoreManager 管理,嗯……分数。
    • Level 类包含关于游戏关卡的信息。
    • 现在我们进入类构造函数。在这里,我们正在为首次使用做准备,因此许多事情都在构造函数中设置。例如,**contentManager** 首先被实例化,然后就可以使用了。**timer** 是 XNA Framework 的一个实例
        .
        .
        .
        public partial class GamePage : PhoneApplicationPage
        {
            ContentManager contentManager;
            GameTimer timer;
            SpriteBatch spriteBatch;
                    
            Camera2d camera;
            
            // For rendering the XAML onto a texture
            UIElementRenderer elementRenderer;
    
            GameSettings settings = new GameSettings();
            ScoreManager scoreManager = new ScoreManager();
            GameStateMachine stateMachine = new GameStateMachine();
            List<Level> levels = new List<Level>();
            int CurrentLevelNumber = 1;
            double minUpdateTimeSpanMs = 20;
            double accumulatedUpdateTimeSpanMs = 0;
    
            double minChaseTimeSpanMs = 500;
            double accumulatedChaseTimeSpanMs = 0;
    
            Texture2D seaTexture;
    
            public GamePage()
            .
            .
            .
    

    类。这个类允许控制两个重要事件:**Update** 和 **Draw**。这里我们注意到事件是如何绑定到方法的。定时器然后是 **camera** 实例,它旨在每次只显示迷宫的一部分。**TouchPanel.EnabledGestures** 属性被设置为允许手势:滑动、长按和点击。页面的OnUpdateOnDraw被设置为 scoreManager 实例(稍后用于 Silverlight 数据绑定)。最后,我们加载游戏关卡。数据上下文 (DataContext)然后是加载和初始化我们的内容。这是通过页面的

            public GamePage()
            {
                InitializeComponent();
    
                // Get the content manager from the application
                contentManager = (Application.Current as App).Content;
    
                // Create a timer for this page
                timer = new GameTimer();
                timer.UpdateInterval = TimeSpan.FromTicks(166667);
                timer.Update += OnUpdate;
                timer.Draw += OnDraw;
    
                camera = new Camera2d(2, 0, settings.ScreenHeight, settings.ScreenWidth, settings.CameraCenter);
    
                // Use the LayoutUpdate event to know when the page layout 
                // has completed so that we can create the UIElementRenderer.
                LayoutUpdated += new EventHandler(GamePage_LayoutUpdated);
    
                TouchPanel.EnabledGestures = GestureType.Flick | GestureType.Hold | GestureType.Tap;
    
                this.DataContext = scoreManager;
    
                LoadLevels();
            }
    

    事件函数完成的。OnNavigatedTo方法由当前游戏关卡的更新启动。请注意,**CurrentLevel.Update** 方法并非始终调用,而仅在经过的时间达到预设的时间量时调用。这样做是为了避免不必要的处理。

            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                // Set the sharing mode of the graphics device to turn on XNA rendering
                SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
    
                // Create a new SpriteBatch, which can be used to draw textures.
                spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
    
                // Start the timer
                timer.Start();
    
                foreach (var level in levels)
                {
                    level.Initialize();
                }
    
                seaTexture = contentManager.Load<Texture2D>(string.Format("{0}/Sea", settings.SpritesRoot));
    
                base.OnNavigatedTo(e);
    
                scoreManager.Level = CurrentLevelNumber;
                stateMachine.GameStateChanged += new GameStateMachine.GameStateChangedHandler(stateMachine_GameStateChanged);
                stateMachine.ChangeState(GameState.PreparingToStartLevel);
            }
    

    OnUpdate然后是搜索算法,鱿鱼用它来追逐蜗牛。这是通过调用

            private void OnUpdate(object sender, GameTimerEventArgs e)
            {
                UpdateLevel(e);
    
                ChaseSnail(e);
    
                HandleInput();
    
                UpdateCamera();
            }
    
    
            private void UpdateLevel(GameTimerEventArgs e)
            {
                accumulatedUpdateTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
                if (accumulatedUpdateTimeSpanMs > minUpdateTimeSpanMs)
                {
                    accumulatedUpdateTimeSpanMs = 0;
    
                    CurrentLevel.Update(e.ElapsedTime);
                }
            }
    

    ChaseSnail在每个鱿鱼对象上完成的。下面的代码显示,这仅在游戏处于Playing状态时进行,否则就没有理由调用该函数。然后是游戏输入的验证。每当用户执行 **Flick** 手势时,蜗牛就会向上、向下、向左和向右移动。当用户点击屏幕时,蜗牛会停止。

        private void ChaseSnail(GameTimerEventArgs e)
        {
            if (stateMachine.CurrentSate == GameState.Playing)
            {
                accumulatedChaseTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
                if (accumulatedChaseTimeSpanMs > minChaseTimeSpanMs)
                {
                    accumulatedChaseTimeSpanMs = 0;
    
                    CurrentLevel.Squids.ForEach(x =>
                    {
                        if (x.ChaseMovementQueue.Count == 0)
                        {
                            x.ChaseSnail();
                        }
                    });
                }
            }
        }
    

    最后是摄像机更新。通常,摄像机会尝试跟随鱿鱼的移动。但是,当鱿鱼到达迷宫的边缘时,摄像机就会停止移动。因此,摄像机移动的区域是有限的。

            private void HandleInput()
            {
                //Check if touch Gesture is available  
                if (TouchPanel.IsGestureAvailable)
                {
                    // Read the gesture so that you can handle the gesture type  
                    GestureSample gesture = TouchPanel.ReadGesture();
                    switch (gesture.GestureType)
                    {
                        case GestureType.Flick:
                            var x = gesture.Delta.X;
                            var y = gesture.Delta.Y;
    
                            CurrentLevel.Flick(x, y);
                            break;
                        case GestureType.Tap:
                            CurrentLevel.Snail.EnqueueMove(Gesture.Tap);
                            break;
                        default:
                            //do something  
                            break;
                    }
                }
            }
    

    然后我们有了

            private void UpdateCamera()
            {
                var newCameraPos = CurrentLevel.Snail.Position * settings.TileWidth;
                if (newCameraPos.X < camera.ViewportWidth / 4)
                {
                    newCameraPos.X = camera.ViewportWidth / 4;
                }
                else if (newCameraPos.X > CurrentLevel.MapWidth * settings.TileWidth - 
                camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X)
                {
                    newCameraPos.X = CurrentLevel.MapWidth * settings.TileWidth - 
                    camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X;
                }
    
                if (newCameraPos.Y < camera.ViewportHeight / 4)
                {
                    newCameraPos.Y = camera.ViewportHeight / 4;
                }
                else if (newCameraPos.Y > CurrentLevel.MapHeight * settings.TileWidth - 
                camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y)
                {
                    newCameraPos.Y = CurrentLevel.MapHeight * settings.TileWidth - 
                    camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y;
                }
    
                camera.Pos = newCameraPos;
            }
    

    方法。根据游戏状态,它需要不同地绘制。OnDraw当游戏处于非

            private void OnDraw(object sender, GameTimerEventArgs e)
            {
                switch (stateMachine.CurrentSate)
                {
                    case GameState.PreparingToStartLevel:
                    case GameState.GameOver:
                    case GameState.LevelCompleted:
                    case GameState.LevelFailed:
                    case GameState.Paused:
                        DrawCentralMessage(e);
                        break;
                    case GameState.Playing:
                        DrawPlaying(e);
                        break;
                }
            }
    

    状态时,它必须显示消息。这些消息在 Silverlight XAML 中定义,而在这里是渲染 Silverlight 内容与 XNA 一起的方式:**elementRenderer.Render();** 渲染 Silverlight 页面(即构建所有视觉元素),然后 **spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);** 告诉应用程序在游戏的精灵批次中渲染 Silverlight 内容。状态时进行,否则就没有理由调用该函数。DrawPlaying

        private void DrawCentralMessage(GameTimerEventArgs e)
        {
            SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
    
            // Render the Silverlight controls using the UIElementRenderer.
            elementRenderer.Render();
    
            this.spriteBatch.Begin();
            // Using the texture from the UIElementRenderer, 
            // draw the Silverlight controls to the screen.
            spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
            this.spriteBatch.End();
        }
    

    代码为了简单起见,被划分为更小的函数。首先,我们绘制背景,即海洋纹理。

            private void DrawPlaying(GameTimerEventArgs e)
            {
                DrawBackground();
                DrawMaze(e);
                DrawAnimals(e);
                DrawSilverlight();
            }
    

    然后我们在上面绘制迷宫。

            private void DrawBackground()
            {
                SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
    
                this.spriteBatch.Begin();
                // Using the texture from the UIElementRenderer, 
                // draw the Silverlight controls to the screen.
                spriteBatch.Draw(seaTexture, Vector2.Zero, 
                    new xna.Rectangle(0, 0, settings.ScreenWidth, settings.ScreenHeight), 
                    Color.White);
                this.spriteBatch.End();
            }
    

    然后渲染动物。

        private void DrawMaze(GameTimerEventArgs e)
        {
            this.spriteBatch.Begin(SpriteSortMode.BackToFront,
            BlendState.AlphaBlend, null, null, null, null, 
            camera.Transformation(spriteBatch.GraphicsDevice));
            this.spriteBatch.Draw((float)e.ElapsedTime.TotalSeconds, 
            CurrentLevel.Map2D, settings.MapTopLeftCorner, Color.White);
            this.spriteBatch.End();
        }
    

    最后,我们在所有内容之上绘制 Silverlight 纹理。这个 Silverlight 部分只是游戏进行期间出现在屏幕顶部的分数信息。

            private void DrawAnimals(GameTimerEventArgs e)
            {
                this.spriteBatch.Begin(SpriteSortMode.BackToFront,
                BlendState.AlphaBlend, null, null, null, null, 
                camera.Transformation(spriteBatch.GraphicsDevice));
                CurrentLevel.Snail.Draw(e.ElapsedTime, spriteBatch, 
                    settings.MapTopLeftCorner, 0);
                foreach (var pearl in CurrentLevel.Pearls)
                {
                    pearl.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
                }
                foreach (var starFish in CurrentLevel.StarFishes)
                {
                    starFish.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
                }
                foreach (var squid in CurrentLevel.Squids)
                {
                    squid.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
                }
                this.spriteBatch.End();
            }
    

    您可以在 MSDN 的文章 "How to: Combine Silverlight and the XNA Framework in a Windows Phone Application" 中阅读有用的进一步信息。

            private void DrawSilverlight()
            {
                // Render the Silverlight controls using the UIElementRenderer.
                elementRenderer.Render();
                this.spriteBatch.Begin();
                // Using the texture from the UIElementRenderer, 
                // draw the Silverlight controls to the screen.
                spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
                this.spriteBatch.End();
            }
    

    这款游戏使用了经典的寻路算法,称为“A*”(发音为“A Star”)。根据维基百科

    被鱿鱼追赶

    当 A* 遍历图时,它会沿着已知成本最低的路径前进,并一路维护一个按顺序排列的备用路径段的优先级队列。如果在任何点,正在遍历的路径段的成本高于遇到的另一个路径段,它就会放弃成本较高的路径段,转而遍历成本较低的路径段。这个过程会一直持续到到达目标。

    A* 搜索用于从起始节点找到机器人运动规划问题中的目标节点的路径的说明。空心圆代表开放集中的节点,即尚未探索的节点,实心圆代表闭集中的节点。闭合节点上的颜色指示与起点的距离:越绿,越远。首先可以看到 A* 在直线方向上朝着目标移动,然后在遇到障碍物时,它会探索开放集中的节点。

    (来源:维基百科)

    上述算法是我们问题的完美解决方案。幸运的是,我从 Code Project 借鉴了 Sacha Barber 的一个精彩贡献(涉及查找伦敦地铁任何两个站点之间的最佳路径),并制作了自己的版本。我只需要改变概念:Sacha 的文章涉及一个人试图在当前站点和目标站点之间找到最佳路径,同时尊重这些站点之间的地理连接。另一方面,在 Snail Quest 中,人由鱿鱼代表。目标站点是蜗牛所在的单元格,站点是迷宫内的空单元格,并且代替站点之间的连接,我们现在有了空单元格之间的连接,这些就是迷宫中的“走廊”。

    public List<MovementType> DoSearch(Vector2 squidPosition, Vector2 snailPosition)
            {
                pathsSolutionsFound = new List<List<Vector2>>();
                pathsAgenda = new List<List<Vector2>>();
    
                List<Vector2> pathStart = new List<Vector2>();
                pathStart.Add(squidPosition);
                pathsAgenda.Add(pathStart);
    
                while (pathsAgenda.Count() > 0 && pathsAgenda.Count() < 100)
                {
                    List<Vector2> currPath = pathsAgenda[0];
                    pathsAgenda.RemoveAt(0);
                    if (currPath.Count(
                        x => x.Equals(snailPosition)) > 0)
                    {
                        pathsSolutionsFound.Add(currPath);
                        break;
                    }
                    else
                    {
                        Vector2 currPosition = currPath.Last();
                        List<Vector2> successorPositions =
                            GetSuccessorsForPosition(currPosition);
    
                        foreach (var successorPosition in successorPositions)
                        {
                            if (!currPath.Contains(successorPosition) &&
                                pathsSolutionsFound.Count(x => x.Contains(successorPosition)) == 0)
                            {
                                List<Vector2> newPath = new List<Vector2>();
                                foreach (var station in currPath)
                                    newPath.Add(station);
    
                                newPath.Add(successorPosition);
                                pathsAgenda.Add(newPath);
                            }
                        }
                    }
                }
    
                //Finally, get the best Path, this should be the 1st one found due
                //to the heuristic evaluation performed by the search
                if (pathsSolutionsFound.Count() > 0)
                {
                    var solutionPath = pathsSolutionsFound[0];
    
                    var movementList = new List<MovementType>();
                    var Vector2 = solutionPath[0];
    
                    for (var i = 1; i < solutionPath.Count(); i++)
                    {
                        var movement = MovementType.None;
    
                        if (solutionPath[i].X > Vector2.X)
                            movement = MovementType.Right;
                        if (solutionPath[i].X < Vector2.X)
                            movement = MovementType.Left;
                        if (solutionPath[i].Y > Vector2.Y)
                            movement = MovementType.Bottom;
                        if (solutionPath[i].Y < Vector2.Y)
                            movement = MovementType.Top;
    
                        movementList.Add(movement);
    
                        Vector2 = solutionPath[i];
                    }
    
                    return movementList;
                }
                return null;
            }
    

    使用 Mappy Win32 创建关卡地图

    正如我在开发这款游戏初期所发现的,创建具有重复块的场景是一项非常无聊的任务。幸运的是,有一个非常有趣的地图工具供游戏开发者使用:Mappy Win32。它允许使用一小组常用图形块开发大型游戏场景。所有老派游戏(想想马里奥兄弟、吃豆人、索尼克等)都是使用有限数量的块(或通常称为“图块”)制作的。

    您可以在此网站免费下载 Mappy 工具。

    让我们看看如何使用 Mappy 创建关卡场景:首先,打开文件菜单,然后选择 **新建地图 (New Map)**。

    然后我们将地图配置为使用 25 x 15 个图块,每个图块 32 x 32 像素。

    下一个重要步骤是导入 **图块条 (Tile Strip)**,这是一组 32 x 32 的块,包含我们地图使用的有限图块。

    最后,我们使用之前导入的块自由绘制我们的地图。

    最后一步是保存文件。我们将其命名为 **Level1.FMP** 并保存在 **\SnailRun\SnailRun\SnailRunLibContent\Maps\** 文件夹中。请注意,该文件现在是我们游戏内容的一部分。

    但是等等,这个 .FMP 扩展名是 Mappy 应用程序专有的。XNA 如何导入/处理这种文件?答案是,**它不能**。它根本不知道 .FMP 扩展名。这就是为什么我们必须自己动手。幸运的是,又有一些聪明人已经为我们完成了艰巨的工作。

    Mappy 文件的 XNA 内容管道扩展

    尽管 XNA Framework 可以处理各种文件,但不幸的是,它可以读取和处理的文件数量有限。这就是为什么,如果涉及到一种新类型的文件,我们就需要自己实现一个内容管道扩展。幸运的是,我在 Codeplex 上找到了 XNA 内容管道扩展到 Mappy 地图 (.FMP),由巴西游戏开发者 **Luciano José** 为 Windows 和 XBox 平台开发。

    “**项目描述** 这个 XNA 库到 Mappy 地图有助于 XNA 开发者将 Mappy 工具中制作的 Tile Map 与您的 XNA 项目(Windows 和 Xbox 360)集成。
    该库允许您只需将您的存档 (.FMP) 拖放到 XNA 项目(Windows 和 Xbox 360)的内容项目中。
    使用 Mappy 工具(http://www.tilemap.co.uk/mappy.php)创建您的图块地图,以便与此 XNA 库集成。
    在 SharpGames(巴西 XNA 社区)上查看我关于该库的文章(葡萄牙语)。
    http://www.sharpgames.net/Artigos/Artigo/tabid/58/selectmoduleid/376/ArticleID/1585/reftab/54/Default.aspx
    访问巴西 XNA 社区:http://www.sharpgames.net/

    它的工作原理是读取 .FMP 文件并读取其中的底层结构/对象。这样我们就可以有效地“读取”地图,并利用数据来寻找路线、测试与障碍物的碰撞等。

    不幸的是,正如我所说,**XNA 内容管道扩展 for Mappy** 仅针对 Windows 和 XBox 平台。但是经过一些时间,我设法将其移植到 Windows Phone 平台并使其正常工作。然后我以发布模式对其进行了编译,并将其作为引用包含在项目中。

    在将 **Level1.FMP** 添加到 Content 项目后,您仍然需要配置 **内容处理器属性**。

    最终,一切都工作正常,每个人都高兴。

    最终思考

    就是这样!我希望您喜欢这款游戏和这里介绍的概念。如果您有任何问题、投诉或想法,请在下方留言。

    历史

    • 2012-02-29:初始版本。
    © . All rights reserved.