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

Windows Phone 迷宫

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (52投票s)

2011年10月30日

CPOL

10分钟阅读

viewsIcon

139297

downloadIcon

53839

一个使用加速度计模拟器和 Farseer 物理引擎的 Windows Phone 应用程序

目录

引言

上个月(2011年9月),我收到一个好消息:Windows Phone SDK 7.1 发布了。我打开了“新功能”会话,它充满了很棒的新功能。我特别感兴趣的是新的 Windows Phone 模拟器,它现在能够模拟设备传感器,例如 Windows Phone 加速度计

本文旨在展示如何使用这个新的加速度计模拟功能,并附带一个使用它的简单应用程序。

系统要求

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

  • 适用于 Windows Phone 的 Visual Web Developer 2010 Express
    无论您是熟悉还是初次接触 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 设备上的应用程序和游戏所需的所有工具。
  • Windows Phone SDK 7.1 中的新功能

    新的 Windows Phone SDK 7.1 和新文档现在包含许多新功能

    • 多目标和应用兼容性
    • 多任务处理
    • 执行模型和快速应用切换
    • 闹钟和提醒
    • 后台代理
    • 后台文件传输
    • 后台音频
    • 媒体
    • Silverlight 4
    • 传感器
    • 套接字支持
    • 网络信息
    • 推送通知
    • 动态磁贴
    • Silverlight 和 XNA 集成
    • 应用性能分析
    • Visual Basic 支持
    • 广告
    • WebBrowser 控件
    • 设备状态
    • 本地数据库
    • 独立存储资源管理器
    • 启动器和选择器
    • 联系人和日历
    • 加密凭证存储
    • 用户体验设计指南
    • 认证要求
    • 市场测试套件
    • 相机
    • 图片扩展性
    • 搜索扩展性
    • 应用程序栏
    • 屏幕键盘
    • 系统托盘和进度指示器
    • OData 客户端
    • 全球化和本地化
    • 创建试用应用程序

    在所有这些功能中,本文将介绍的是传感器。但不是所有类型的传感器(因为“传感器”包括加速度计、陀螺仪传感器、组合运动和旋转速率)。目前,我们只玩加速度计。希望在不久的将来,我们可以专注于 WP7 的其他功能。

    加速度计模拟器

    访问新的加速度计模拟器非常容易。首先,启动 Windows Phone 模拟器。然后将鼠标移到模拟器窗口上。此时,将显示您在模拟器上一版本中找到的相同垂直图标栏。不同之处在于您可以在图标栏底部找到的“>>”图标。单击它,将打开一个名为“附加工具”的新窗口

    只有第一个选项卡“加速度计”对我们很重要。它有设备的3D渲染(请注意,此渲染不显示模拟器的相同屏幕)。这是一个带有静态瓷砖的假屏幕。但是,嘿,微软,如果这里有实际的设备屏幕,那会很酷,你不觉得吗?

    加速度计模拟器中需要注意的第二件事是方向下拉列表。我们首先需要选择正确的方向(纵向或横向,以及站立或平放)才能正确执行模拟。我们的应用程序只有在设备处于水平位置时才能正常工作,因此您必须选择纵向平放横向平放方向。

    加速度计模拟器中需要注意的第二件事是窗口中心的小红圈。拖动它并在屏幕上移动,以模拟加速度计。

    当您移动加速度计控制点时,X、Y 和 Z 坐标会发生变化并显示在屏幕上。此外,这些数据还会实时传递给 Windows Phone 模拟器,因此您所做的任何更改都可以被应用程序读取。

    在代码中,加速度计功能是Microsoft.Devices.Sensors命名空间

    using Microsoft.Devices.Sensors;

    应用程序启动后,我们立即启动加速度计,它每20毫秒触发一次更新事件

            private void StartAccelerometer()
            {
                if (accelerometer == null)
                {
                    // Instantiate the accelerometer.
                    accelerometer = new Accelerometer();
    
                    // Specify the desired time between updates. The sensor accepts
                    // intervals in multiples of 20 ms.
                    accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20);
    
                    accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(accelerometer_CurrentValueChanged);
                }
    
                accelerometer.Start();
            }
    

    当加速度计传感器值改变时,我们只需改变重力参数世界对象。World 对象用于管理 Farseer 中的所有其他对象

            void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
            {
                // Note that this event handler is called from a background thread
                // and therefore does not have access to the UI thread. To update 
                // the UI from this handler, use Dispatcher.BeginInvoke() as shown.
    
                if (World != null)
                {
                    World.Gravity = new Vector2(e.SensorReading.Acceleration.Y * -ACC_GRAVITY_COEF, e.SensorReading.Acceleration.X * -ACC_GRAVITY_COEF);
                }
            }
    

    Farseer 物理引擎

    Farseer 是一个出色的开源物理引擎,基于原始的 Box 2D 开源项目(顺便说一句,愤怒的小鸟游戏就使用了 Box2D)。不同之处在于 Box2D 用 C++ 编写(并已移植到多种语言),而 Farseer 用 C# 编写,目标是 Silverlight 和 XNA。此外,Farseer 网站声称此框架除了原始 Box2D 版本提供的功能外,还有许多其他功能。

    创建第一个对象:球

    默认情况下,Farseer 将圆形对象渲染为带有特定边框和纹理的椭圆形。我们的球不过是一个 Farseer 圆形,用钢球的静态纹理渲染而成。我们首先加载球的纹理并定义球的初始位置

            public override void LoadContent()
            {
                base.LoadContent();            
                LoadTextures();
                DefineScreenRectangle();
                InitializeBall();
                InitializeLabyrinth();
                StartAccelerometer();
            }
    
            private void InitializeLabyrinth()
            {
                List<Vertices> verticesList = GetVerticesFromLabyrinthTexture();
                ScaleVertices(verticesList);
                CreateLabyrinthBody(verticesList);
            }
    
            private void CreateLabyrinthBody(List<Vertices> verticesList)
            {
                //Create a single body with multiple fixtures
                labyrinth = BodyFactory.CreateCompoundPolygon(World, verticesList, 1f, BodyType.Dynamic);
                labyrinth.BodyType = BodyType.Dynamic;
                labyrinth.IsStatic = true;
            }
    
            private void ScaleVertices(List<Vertices> verticesList)
            {
                //scale the vertices from graphics space to sim space
                Vector2 vertScale = new Vector2(ConvertUnits.ToSimUnits(1)) * SCALE_FOR_WP_RESOLUTION;
                foreach (Vertices vertices in verticesList)
                {
                    vertices.Scale(ref vertScale);
                }
            }
    
            private List<Vertices> GetVerticesFromLabyrinthTexture()
            {
                Vertices textureVertices;
                //Create an array to hold the data from the texture
                uint[] data = new uint[labyrinthTexture.Width * labyrinthTexture.Height];
    
                //Transfer the texture data to the array
                labyrinthTexture.GetData(data);
    
                //Find the vertices that makes up the outline of the shape in the texture
                textureVertices = PolygonTools.CreatePolygon(data, labyrinthTexture.Width, false);
    
                //The tool return vertices as they were found in the texture.
                //We need to find the real center (centroid) of the vertices for 2 reasons:
    
                //1. To translate the vertices so the polygon is centered around the centroid.
                Vector2 centroid = -textureVertices.GetCentroid();
                textureVertices.Translate(ref centroid);
    
                //2. To draw the texture the correct place.
                labyrinthOrigin = -centroid;
    
                //We simplify the vertices found in the texture.
                textureVertices = SimplifyTools.ReduceByDistance(textureVertices, 4f);
    
                //Since it is a concave polygon, we need to partition it into several smaller convex polygons
                return BayazitDecomposer.ConvexPartition(textureVertices);
            }
    
            private void InitializeBall()
            {
                Vector2 startPosition = new Vector2(-20f, -11f);
                Vector2 endPosition = new Vector2(20, -11f);
                ball = new Ball(World, this, startPosition, 1.5f, false);
                ball.BallCollided += new Labyrinth.Ball.BallCollisionEventHandler(ball_BallCollided);
            }
    
            private void DefineScreenRectangle()
            {
                var rectangle = ScreenManager.GraphicsDevice.Viewport.Bounds;
                screenRectangle = new Rectangle(-rectangle.Width, -rectangle.Height, rectangle.Width * 2, rectangle.Height * 2);
            }
    
            private void LoadTextures()
            {
                backgroundTexture = ScreenManager.Content.Load<Texture2D>("Common/woodenbackground");
                ballofSteelTexture = ScreenManager.Content.Load<Texture2D>("Samples/ballofsteel");
                ballShadowTexture = ScreenManager.Content.Load<Texture2D>("Samples/ballshadow");
                woodenForegroundTexture = ScreenManager.Content.Load<Texture2D>("Samples/woodenforeground");
                labyrinthShadowTexture = ScreenManager.Content.Load<Texture2D>("Samples/labyrinthshadow");
                labyrinthTexture = ScreenManager.Content.Load<Texture2D>("Samples/labyrinth");
            }
    

    然后我们使用预加载的纹理绘制球

            public override void Draw(GameTime gameTime)
            {
                BeginSpriteBatch();
                DrawBackground();
                DrawLabyrinthShadow();
                DrawBall();
                DrawLabyrinth();
                EndSpriteBatch();
                base.Draw(gameTime);
            }
    
            private void DrawLabyrinth()
            {
                ScreenManager.SpriteBatch.Draw(labyrinthTexture, ConvertUnits.ToDisplayUnits(labyrinth.Position),
                                               null, Color.Azure, labyrinth.Rotation, labyrinthOrigin, SCALE_FOR_WP_RESOLUTION, SpriteEffects.None, 0f);
            }
    
            private void DrawBall()
            {
                var ballShadowVector = new Vector2(World.Gravity.X, World.Gravity.Y) * ACC_BALL_SHADOW_COEF;
                ball.Draw(ballofSteelTexture, ballShadowTexture, ballShadowVector);
            }
    
            private void DrawLabyrinthShadow()
            {
                var shadowVector = new Vector2(World.Gravity.X, World.Gravity.Y) * -ACC_SHADOW_COEF;
    
                ScreenManager.SpriteBatch.Draw(labyrinthShadowTexture, ConvertUnits.ToDisplayUnits(labyrinth.Position),
                null, Color.Azure, labyrinth.Rotation, labyrinthOrigin - shadowVector, SCALE_FOR_WP_RESOLUTION, SpriteEffects.None, 0f);
            }
    
            private void DrawBackground()
            {
                ScreenManager.SpriteBatch.Draw(backgroundTexture, screenRectangle, Color.White);
            }
    
            private void EndSpriteBatch()
            {
                ScreenManager.SpriteBatch.End();
            }
    
            private void BeginSpriteBatch()
            {
                ScreenManager.SpriteBatch.Begin(0, null, null, null, null, null, Camera.View);
            }
    

    设置第二个对象:迷宫

    迷宫本身通过3种纹理渲染:木质背景、迷宫阴影和迷宫木质边框

    因此,阴影有助于根据加速度计数据提供的旋转来模仿迷宫的3D外观和感觉

    迷宫纹理首先像前面提到的钢球一样被加载

        public override void LoadContent()
        {
            .
            .
            .
            //load texture that will represent the physics body
            labyrinthTexture = ScreenManager.Content.Load<Texture2D>("Samples/labyrinth");
            .
            .
            .
        }
    

    现在我们通过连接一系列矩形来创建迷宫,对吗?错了!与球不同,迷宫是从迷宫的纹理图像创建的。这是 Farseer 引擎的一个非常棒的功能,它使我们的生活变得轻松多了!Farseer 内置函数从我们的迷宫平面图像创建一个实体对象,前提是我们把空的地方留成透明颜色,就像这样

    现在想象一下创建新关卡是多么容易……太棒了,不是吗?Farseer 函数将为我们完成繁重的工作,在下面的代码中,最值得一提的部分是 BayazitDecomposer.ConvexPartition 方法,它从一个大的凹多边形中创建较小的凸多边形

        public override void LoadContent()
        {
            .
            .
            .
    
            //Create an array to hold the data from the texture
            uint[] data = new uint[labyrinthTexture.Width * labyrinthTexture.Height];
    
            //Transfer the texture data to the array
            labyrinthTexture.GetData(data);
    
            //Find the vertices that makes up the outline of the shape in the texture
            Vertices textureVertices = PolygonTools.CreatePolygon(data, labyrinthTexture.Width, false);
    
            //The tool return vertices as they were found in the texture.
            //We need to find the real center (centroid) of the vertices for 2 reasons:
    
            //1. To translate the vertices so the polygon is centered around the centroid.
            Vector2 centroid = -textureVertices.GetCentroid();
            textureVertices.Translate(ref centroid);
    
            //2. To draw the texture the correct place.
            labyrinthOrigin = -centroid;
    
            //We simplify the vertices found in the texture.
            textureVertices = SimplifyTools.ReduceByDistance(textureVertices, 4f);
    
            //Since it is a concave polygon, we need to partition it into several smaller convex polygons
            List<Vertices> list = BayazitDecomposer.ConvexPartition(textureVertices);
    
            //Adjust the scale of the object for WP7's lower resolution
    
            scale = 2f;
    
            //scale the vertices from graphics space to sim space
            Vector2 vertScale = new Vector2(ConvertUnits.ToSimUnits(1)) * scale;
            foreach (Vertices vertices in list)
            {
                vertices.Scale(ref vertScale);
            }
    
            //Create a single body with multiple fixtures
            labyrinth = BodyFactory.CreateCompoundPolygon(World, list, 1f, BodyType.Dynamic);
            labyrinth.BodyType = BodyType.Dynamic;
            labyrinth.IsStatic = true;
    
            StartAccelerometer();
        }
    

    触觉反馈,又称振动反馈

    “触觉技术”,或“触觉”,是设备根据游戏中或某种虚拟现实模拟中发生的某些事件,向用户提供的触觉反馈。

    虽然这个项目可能不被称为“虚拟现实”,但它确实试图模仿那些里面有小球的旧式玻璃盖木制迷宫盒子。触觉反馈的加入无疑改善了用户体验,这就是我们使用它的原因。但如何在 Windows Phone 中实现呢?

    智能手机编程的一个优点是,这些小玩意儿拥有许多台式电脑所没有的功能/设备/传感器。因此,在台式电脑编程中,你针对的是键盘/鼠标/屏幕/扬声器/磁盘,而在智能手机中,你现在可以访问内置的加速度计/振动/陀螺仪/多点触控等等。

    我们在 Windows Phone 中实现触觉反馈,只需访问 Microsoft.Devices.VibrateController 类 (http://msdn.microsoft.com/en-us/library/microsoft.devices.vibratecontroller(v=vs.92).aspx),并根据需要启动并保持振动。就这么简单。但是我们何时以及振动设备多久呢?

    首先,我们必须处理球与迷宫之间的碰撞。然后,我们应该测量碰撞的影响,最后告诉手机根据影响进行振动。冲击越强烈,振动持续的时间就越长。

    至于实现,我们首先使用对 Microsoft.Devices 命名空间的引用

    using Microsoft.Devices;
    

    然后我们声明两个常量,一个用于振动的最大间隔(毫秒),另一个用于启动振动所需的最小速度。

            private const float MAX_VIBRATION_MILLISECS = 16f;
            private const float MIN_DELTAVELOCITY_FOR_VIBRATING_DEVICE = 8f;
    

    接下来,我们实现球实例的 BallCollided 事件

            private void InitializeBall()
            {
                .
                .
                .
                ball.BallCollided += new Labyrinth.Ball.BallCollisionEventHandler(ball_BallCollided);
            }
    
            void ball_BallCollided(float deltaVelocity)
            {
                if (deltaVelocity > MIN_DELTAVELOCITY_FOR_VIBRATING_DEVICE)
                {
                    VibrateController.Default.Start(TimeSpan.FromMilliseconds(
                        deltaVelocity > MAX_VIBRATION_MILLISECS ? MAX_VIBRATION_MILLISECS : deltaVelocity)
                    );
                }
            }
    

    请注意,并非所有碰撞都会引起振动。球的撞击速度必须符合预设的最小值,并且振动时间不能过长。我们施加这些限制是因为否则反馈将不如我们预期的真实。例如,轻微的撞击会使手机一直振动,而大的撞击会产生长时间的振动。这会让用户感到恼火。我发现最好的方法是将振动保持在这两个限制之下。

    每秒60帧,感谢 Mango 更新!

    如果你在 Windows Phone Mango 更新之前尝试开发游戏,你可能会因为 XNA 只能以每秒 30 帧的速度运行(无论你的硬件有多强大)而感到失望。这在三年前可能是一个不错的规格,但对于当前的智能手机平台来说,30 帧每秒对于动作游戏来说是糟糕的。

    幸运的是,Mango 更新消除了这一障碍,让我们能够为我们的硬件启用尽可能高的帧率,目前是 60 fps。但是要从这个功能中受益,您必须首先将您的应用程序配置为以 60 fps 运行。

    首先,在你的 Game 类中实现 GraphicsDeviceManagerPreparingDeviceSettings 事件

        public class LabyrinthGame : Game
        {
            private GraphicsDeviceManager _graphics;
    
            public LabyrinthGame()
            {
                .
                .
                .
                _graphics.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(_graphics_PreparingDeviceSettings);
                .
                .
                .
            }
    

    然后,在 PreparingDeviceSettings 事件内部,将 PresentationInterval 设置为 PresentInterval.One

            void _graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
            {
                e.GraphicsDeviceInformation.PresentationParameters.PresentationInterval = PresentInterval.One;
            }
    

    欲了解更多信息,请参阅“Windows Phone OS 7.1 中更改的 API”文档

    API Windows Phone OS 7.0 行为 Windows Phone OS 7.1 行为
    PresentationParameters.PresentationInterval 属性 Windows Phone OS 7.0 上的 XNA Framework 针对 30 Hz 的刷新率,无论您设置了哪个 PresentationInterval。 Windows Phone OS 7.1 上的 XNA Framework 支持本机设备刷新率,最高可达 60 Hz。如果您将 PresentationParameters.PresentationInterval 属性设置为 PresentInterval.One,则使用 PresentInterval.One 值,针对本机设备刷新率。

    就这样。也许在未来的更新中,这将是默认设置,但目前请记住在您的应用程序中启用它。

    看起来很酷,但这还不是一个游戏

    是的,到目前为止,这还不是一个“迷宫游戏”,这只是“迷宫”。我在这里的目的不是创建一个完整的游戏,只是指明方向。现在你可以创建一个像样的分数控制、“地板上的洞”、陷阱、风扇、线圈、电梯,各种各样的障碍等等。我认为这有很大的潜力成为一个非常棒的游戏。

    想法,想法,想法...

    从现在开始,您可以使用 Windows Phone 加速度计和/或 Farseer 物理引擎创建新的应用程序。这里有一些您可能正在考虑的想法,例如

    • 一个像《愤怒的小鸟》一样的游戏
    • 一个像《俄罗斯方块》一样的游戏(使用加速度计定位方块)
    • 飞行模拟器
    • 第一人称射击游戏
    • 台球游戏
    • 火箭发射游戏
    • 赛车游戏

    这只是其中一部分。这些是我在几秒钟内就能想到的清单。这些技术 certainly 具有巨大的潜力。

    最终思考

    我们很高兴“Mango”更新之后的 Windows Phone SDK 7.1 为我们开发者带来了许多新工具。我相信这将有助于提高 Windows Phone 应用商店中应用程序的数量和质量。

    如果您有兴趣为应用商店开发新的、令人兴奋的应用程序,我强烈建议您随时了解最新的工具和技术。首先,阅读“Windows Phone SDK 7.1 中的新功能”文档。此外,下载该页面上的示例,并花时间了解其工作原理。

    请记住,这个一岁的应用商店中“只有”36,000多个应用程序,但根据 Gartner Group 的说法,Windows Phone 倾向于在未来几年占据更重要的位置。这为您提供了学习和为该平台创建出色应用程序的绝佳机会。

    历史

    • 2011-10-31: 初始版本。
    • 2011-11-06: 代码重构。新增:触觉反馈。
    • 2011-11-08: 新增:“每秒60帧”部分。
    © . All rights reserved.