AndEngine-安卓游戏开发秘籍-全-
AndEngine 安卓游戏开发秘籍(全)
原文:
zh.annas-archive.org/md5/DC9ACC22F79E7DA8DE93ED0AD588BA9A
译者:飞龙
前言
AndEngine 是一个卓越的、功能齐全的、免费的、开源的 Android 平台 2D 框架。它是少数几个持续被独立开发者和专业开发者用来创建时尚有趣游戏的 Android 平台 2D 框架之一,甚至一些市场上最成功的游戏也使用了它。然而,要取得成功,仅仅使用特定的框架是不够的。
AndEngine for Android Game Development Cookbook 提供了许多关于 AndEngine 最重要方面的信息性演练,这些方面属于一般游戏编程级别。这本书涵盖了从 AndEngine 游戏的生命周期到在场景中放置精灵并移动它们,一直到创建可破坏物体和光线投射技术等内容。更重要的是,这本书完全基于 AndEngine 最新的、最高效的 Anchor-Center 分支。
这本书涵盖的内容。
第一章,AndEngine 游戏结构,涵盖了使用 AndEngine 进行游戏开发的重要方面,关于大多数游戏需要生存的核心组件。从音频、纹理、AndEngine 生命周期、保存/加载游戏数据等,这一章都有所涉及。
第二章,使用实体,开始让我们熟悉 AndEngine 的 Entity
类及其子类型,如精灵、文本、基元等。Entity
类是 AndEngine 的核心组件,它允许代码中的对象在屏幕上显示。更具体地说,这一章包括 Entity
类中最重要方法的列表,以使我们能够完全控制实体的行为、反应或它们的外观。
第三章,设计你的菜单,介绍了一些移动游戏中菜单设计较常见的方面。本章涵盖的主题包括创建按钮,为菜单添加主题音乐,创建视差背景和菜单屏幕导航。本章中的主题很容易被用在游戏的其他区域。
第四章,使用摄像头,讨论了 AndEngine 中包含的各种关于游戏摄像头和引擎如何查看游戏场景的选项。我们从不同的摄像头对象开始,以便让我们正确理解每种摄像头的优点,从而做出有见地的决定。然后,我们继续涵盖摄像头的移动和缩放,创建超大背景,创建抬头显示,甚至介绍分屏游戏引擎以应对更复杂的游戏设计。
第五章,场景和图层管理,展示了如何创建一个健壮的场景管理框架,该框架包含特定场景的加载屏幕和动画图层。本章中的管理场景使用资源管理器,并且非常易于定制。
第六章,物理学的应用,探索了使用 Box2D 物理扩展创建 AndEngine 物理模拟的各种技术。本章的内容涵盖了 Box2D 物理世界的基本设置:体类型、类别过滤、具有多个固定装置的物体、基于多边形的物体、力、关节、布娃娃、绳索、碰撞事件、可破坏物体和光线投射。
第七章,使用更新处理器,展示了每次引擎更新时调用的更新处理器的使用方法。本章的内容展示了如何注册基于实体的更新处理器、条件更新和创建游戏计时器。
第八章,最大化性能,介绍了一些在提高任何 Android 游戏性能时最有效的高级实践。本章涵盖了涉及音频、图形/渲染和一般内存管理的优化技术,以帮助在必要时减轻性能问题。
第九章,AndEngine 扩展概述,在这一章中我们讨论了一些更受欢迎的 AndEngine 扩展,根据游戏的不同,这些扩展可能对项目有益。这些扩展并非适合所有人,但对于感兴趣的人来说,本章包含了我们如何着手创建动态壁纸、通过网络服务器和客户端实现多人组件、创建高分辨率 SVG 纹理以及色彩映射纹理的见解。
第十章,更深入了解 AndEngine,提供了几个有用的食谱,这些食谱扩展了前几章介绍的概念。本章的内容包括批量纹理加载、纹理网格、自动阴影、移动平台和绳索桥梁。
附录 A,MagneTank 的源代码,概述了 MagneTank 游戏,通过逐类描述来展示如何设置用 AndEngine 制作完整的游戏。该游戏包括贯穿各章节的许多食谱,并且附带的代码中提供了源代码。
附录 B,附加食谱,书中未包含,但可以通过以下链接免费下载:downloads.packtpub.com/sites/default/files/downloads/8987OS_AppB_Final.pdf
。
你需要为这本书准备什么
《AndEngine for Android Game Development Cookbook》对大多数 AndEngine 开发者都有用。从最初的几章开始,读者将开始学习 AndEngine 的基础知识,即使是中级开发者也能在这些章节中找到有用的提示。随着读者章节的深入,将涉及更难的话题,因此初学者不要跳过。此外,那些尚未过渡到 AndEngine 最新开发分支的中级开发者,在整个书中都能找到关于 GLES1/GLES2 分支与本书讨论的 Anchor-Center 分支之间的差异的有用信息。
建议具备 Java 编程语言的基本理解。
为了执行本书中的各种主题,所需的软件包括用于构建和编译代码的 Eclipse IDE,用于图像绘制/编辑的 GIMP,以及用于 SVG 绘制/编辑的 Inkscape。如果您对它们更熟悉,请随意使用这些产品的替代品。此外,本书假设读者在开始使用食谱之前已经获得了所需的库,包括 AndEngine 及其各种扩展。
本书适合的读者
《AndEngine for Android Game Development Cookbook》面向那些对使用最新版本的 AndEngine 感兴趣的开发者,该版本采用了全新的 GLES 2.0 Anchor-Center 分支。这本书将帮助那些试图进入移动游戏市场,打算发布有趣且刺激的游戏,同时减少进入 AndEngine 开发时不可避免的学习曲线的开发者。
约定
在这本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。
文本中的代码字如下所示:"以最基础的Entity
方法为例,我们将一个Entity
对象附加到一个Scene
对象上。"
代码块设置如下:
float baseBufferData[] = {
/* First Triangle */
0, BASE_HEIGHT, UNUSED, /* first point */
BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */
BASE_WIDTH, 0, UNUSED, /* third point */
/* Second Triangle */
BASE_WIDTH, 0, UNUSED, /* first point */
0, 0, UNUSED, /* second point */
0, BASE_HEIGHT, UNUSED, /* third point */
};
注意
警告或重要注意事项会像这样出现在一个框里。
提示
提示和技巧会像这样出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或可能不喜欢的内容。读者的反馈对我们来说很重要,可以帮助我们开发出您真正能从中获得最大收益的标题。
如需向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,并在邮件的主题中提及书名。
如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors上的作者指南。
客户支持
既然您已经拥有了 Packt 的一本书,我们有一些事情可以帮助您充分利用您的购买。
下载示例代码
您可以从您的账户www.PacktPub.com
下载您购买的所有 Packt 书籍的示例代码文件。如果您在别处购买了这本书,可以访问www.PacktPub.com/support
注册,我们会直接将文件通过电子邮件发送给您。
勘误
尽管我们已经竭尽全力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误信息,请通过访问www.packtpub.com/support
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误信息得到验证,您的提交将被接受,勘误信息将被上传到我们的网站,或添加到该标题下的现有勘误列表中。任何现有的勘误信息可以通过从www.packtpub.com/support
选择您的标题来查看。
盗版
互联网上版权资料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品的非法副本,请立即提供我们该位置地址或网站名称,以便我们可以寻求补救措施。
如果您有疑似盗版资料的链接,请联系<copyright@packtpub.com>
。
我们感谢您帮助保护我们的作者,以及我们为您带来有价值内容的能力。
问题
如果您在书的任何方面遇到问题,可以联系<questions@packtpub.com>
,我们将尽力解决。
第一章:AndEngine 游戏结构
在本章中,我们将了解构建AndEngine游戏中所需的主要组成部分。主题包括:
-
了解生命周期
-
选择我们的引擎类型
-
选择分辨率策略
-
创建对象工厂
-
创建游戏管理器
-
引入声音和音乐
-
使用不同类型的纹理
-
应用纹理选项
-
使用 AndEngine 字体资源
-
创建资源管理器
-
保存和加载游戏数据
引言
AndEngine 最吸引人的方面是创建游戏的极大便捷性。在首次接触 AndEngine 后,在几周内设计和编码一个游戏并非遥不可及,但这并不意味着它将是一个完美的游戏。如果我们不理解引擎的工作原理,编码过程可能会很繁琐。为了创建精确、有序且可扩展的项目,了解 AndEngine 的主要构建块和游戏结构是一个好主意。
在本章中,我们将介绍 AndEngine 和一般游戏编程中最必要的几个组成部分。我们将查看一些类,这些类将帮助我们快速高效地创建各种游戏的基础。此外,我们还将介绍资源和对象类型之间的区别,这些区别在塑造游戏的整体外观和感觉方面起着最重要的作用。如果需要,建议将本章作为参考资料保存。
了解生命周期
在初始化游戏时,了解操作的顺序是很重要的。游戏的基本需求包括创建引擎、加载游戏资源、以及设置初始屏幕和设置。这就是创建 AndEngine 游戏基础所需的一切。但是,如果我们计划在游戏中实现更多多样性,那么了解 AndEngine 中包含的完整生命周期是明智的。
准备就绪
请参考代码包中名为PacktRecipesActivity
的类。
如何操作…
AndEngine 生命周期包括我们直接负责定义的几个方法。这些方法包括创建EngineOptions
对象,创建Scene
对象,以及用子实体填充场景。这些方法的调用顺序如下:
-
定义
onCreateEngineOptions()
方法:@Override public EngineOptions onCreateEngineOptions() { // Define our mCamera object mCamera = new Camera(0, 0, WIDTH, HEIGHT); // Declare & Define our engine options to be applied to our Engine object EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(), mCamera); // It is necessary in a lot of applications to define the following // wake lock options in order to disable the device's display // from turning off during gameplay due to inactivity engineOptions.setWakeLockOptions(WakeLockOptions.SCREEN_ON); // Return the engineOptions object, passing it to the engine return engineOptions; }
-
定义
onCreateResources()
方法:@Override public void onCreateResources( OnCreateResourcesCallback pOnCreateResourcesCallback) { /* We should notify the pOnCreateResourcesCallback that we've finished * loading all of the necessary resources in our game AFTER they are loaded. * onCreateResourcesFinished() should be the last method called. */ pOnCreateResourcesCallback.onCreateResourcesFinished(); }
-
定义
onCreateScene()
方法:@Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { // Create the Scene object mScene = new Scene(); // Notify the callback that we're finished creating the scene, returning // mScene to the mEngine object (handled automatically) pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
定义
onPopulateScene()
方法:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { // onPopulateSceneFinished(), similar to the resource and scene callback // methods, should be called once we are finished populating the scene. pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理…
在此食谱类中找到的代码是任何 AndEngine 游戏的基础。我们设置了一个主活动类,作为进入我们应用程序的入口点。活动包含 AndEngine 活动生命周期中我们负责的四个主要方法,从创建EngineOptions
选项开始,创建资源,创建场景,以及填充场景。
在第一步中,我们覆盖了引擎的onCreateEngineOptions()
方法。在这个方法内部,我们主要关注实例化Camera
对象和EngineOptions
对象。这两个对象的构造函数允许我们定义应用程序的显示属性。此外,通过调用engineOptions.setWakeLockOptions(WakeLockOptions.SCREEN_ON)
方法,我们阻止了在应用程序不活动期间屏幕自动关闭。
在第二步中,我们继续覆盖onCreateResources()
方法,该方法为我们提供了一个特定方法,用于创建和设置游戏所需的所有资源。这些资源可能包括纹理、声音和音乐以及字体。在这一步和接下来的两步中,我们需要调用相应的方法回调,以继续应用程序的生命周期。对于onCreateResources()
方法,我们必须在方法的最后包含调用pOnCreateResourcesCallback.onCreateResourcesFinished()
。
第三步涉及实例化和设置Scene
对象。设置场景可以像本食谱中显示的那么简单,或者对于更复杂的项目,它可能包括设置触摸事件监听器、更新处理器等。完成场景设置后,我们必须调用pOnCreateSceneCallback.onCreateSceneFinished(mScene)
方法,将我们新创建的mScene
对象传递给引擎,以便在设备上显示。
最后需要处理的步骤包括定义onPopulateScene()
方法。此方法专门用于将子实体附加到场景。与之前的两个步骤一样,我们必须调用pOnPopulateSceneCallback.onPopulateSceneFinished()
以继续剩余的 AndEngine 生命周期调用。
在以下列表中,我们将按照从活动启动到终止时调用的顺序介绍生命周期方法。
启动期间的生命周期调用如下:
-
onCreate
:此方法是 Android SDK 的原生应用程序入口点。在 AndEngine 开发中,此方法只需调用我们BaseGameActivity
类中的onCreateEngineOptions()
方法,然后将返回的选项应用到游戏引擎中。 -
onResume
:这是 Android SDK 的另一个原生方法。在这里,我们从EngineOptions
对象获取唤醒锁设置,然后为引擎的RenderSurfaceView
对象调用onResume()
方法。 -
onSurfaceCreated
:此方法将在我们活动的初始启动过程中调用onCreateGame()
,或者如果活动之前已经部署,则将布尔变量注册为true
以重新加载资源。 -
onReloadResources
:如果我们的应用程序从最小化状态恢复到焦点状态,此方法将重新加载游戏资源。在应用程序首次执行时不会调用此方法。 -
onCreateGame
:这是为了处理 AndEngine 生命周期中接下来三个回调的执行顺序。 -
onCreateResources
:这个方法允许我们声明和定义在启动活动时应用所需的最初资源。这些资源包括但不限于纹理、声音和音乐以及字体。 -
onCreateScene
:在这里,我们处理活动场景对象的初始化。在这个方法中可以附加实体到场景,但为了保持组织性,通常最好在onPopulateScene()
中附加实体。 -
onPopulateScene
:在生命周期中的onPopulateScene()
方法里,我们几乎完成了场景的设置,尽管还有一些生命周期调用会由引擎自动处理。这个方法应该用来定义应用首次启动时场景的视觉结果。注意,此时场景已经被创建并应用到引擎中。如果此时没有加载屏幕或启动画面,并且有许多实体需要附加到场景中,那么在某些情况下可能会看到实体被附加到场景上。 -
onGameCreated
:这表明onCreateGame()
序列已经完成,如有必要,重新加载资源,否则什么都不做。是否重新加载资源取决于在五个生命周期调用之前的onSurfaceCreated
方法中简要提到的布尔变量。 -
onSurfaceChanged
:每次应用的方向从横屏模式变为竖屏模式,或者从竖屏模式变为横屏模式时,都会调用这个方法。 -
onResumeGame
:这是在活动启动周期中最后一个调用的方法。如果我们的活动在没有问题的情况下到达这一点,将调用引擎的start()
方法,使游戏的更新线程活跃起来。
在最小化/终止过程中的生命周期调用如下:
-
onPause
:活动最小化或结束时首先调用的方法。这是原生安卓的暂停方法,它调用RenderSurfaceView
对象的暂停方法,并恢复游戏引擎应用的唤醒锁设置。 -
onPauseGame
:接下来,AndEngine 的onPause()
实现,它只是简单地在引擎上调用stop()
方法,导致引擎的所有更新处理器以及更新线程停止。 -
onDestroy
:在onDestroy()
方法中,AndEngine 会清除由引擎管理类持有的ArrayList
对象中包含的所有图形资源。这些管理类包括VertexBufferObjectManager
类、FontManager
类、ShaderProgramManager
类,以及最后的TextureManager
类。 -
onDestroyResources
:这个方法名称可能有些误导,因为我们已经在onDestroy()
中卸载了大部分资源。这个方法真正的作用是,通过调用相应管理器的releaseAll()
方法,释放所有存储在其中的声音和音乐对象。 -
onGameDestroyed
:最后,我们到达在整个 AndEngine 生命周期中需要调用的最后一个方法。在这个方法中没有太多动作发生。AndEngine 只是将用于 Engine 的mGameCreated
布尔变量设置为false
,表示活动不再运行。
在以下图片中,我们可以看到当创建游戏、最小化或销毁游戏时,生命周期的实际表现:
注意
由于 AndEngine 生命周期的异步性质,在单个启动实例期间可能会多次执行某些方法。这些事件的发生在设备之间是不同的。
还有更多…
在本食谱的前一部分中,我们已经介绍了主要的BaseGameActivity
类。以下类可以作为BaseGameActivity
类的替代品,每个类都有自己的一些细微差别。
LayoutGameActivity
类
LayoutGameActivity
类是一个有用的活动类,它允许我们将 AndEngine 场景图视图集成到普通的 Android 应用程序中。另一方面,使用这个类,我们还可以将原生的 Android SDK 视图,如按钮、滑动条、下拉列表、附加布局或其他任何视图包含到我们的游戏中。然而,使用这种活动最流行的原因是便于在游戏中实现广告,作为一种获取收益的手段。
为LayoutGameActivity
类设置需要几个额外的步骤。
-
在项目的默认布局 XML 文件中添加以下行。这个文件通常称为
main.xml
。以下代码段将 AndEngineRenderSurfaceView
类添加到我们的布局文件中。这是将在设备上显示我们游戏的视图:<org.andengine.opengl.view.RenderSurfaceView android:id="@+id/gameSurfaceView" android:layout_width="fill_parent" android:layout_height="fill_parent"/>
-
这种活动类型的第二个也是最后一个额外步骤是在第一步中引用布局 XML 文件和
RenderSurfaceView
,在LayoutGameActivity
重写方法中。以下代码假设布局文件在res/layout/
文件夹中称为main.xml
;在这种情况下,可以在完成第一步后将其复制/粘贴到我们的LayoutGameActivity
类中:@Override protected int getLayoutID() { return R.layout.main; } @Override protected int getRenderSurfaceViewID() { return R.id.gameSurfaceView; }
SimpleBaseGameActivity
和SimpleLayoutGameActivity
类
如建议的那样,SimpleBaseGameActivity
和SimpleLayoutGameActivity
类使重写生命周期方法变得更容易处理。它们不要求我们重写onPopulateScene()
方法,而且,在我们定义完重写的方法后,我们也不需要调用方法回调。使用这些活动类型,我们可以简单地添加未实现的生命周期方法,AndEngine 会为我们处理回调。
SimpleAsyncGameActivity
类
我们将要讨论的最后一个游戏活动类是SimpleAsyncGameActivity
类。这个类包括三个可选的生命周期方法:onCreateResourcesAsync()
、onCreateSceneAsync()
和onPopulateSceneAsync()
,以及通常的onCreateEngineOptions()
方法。这个活动与其他活动的主要区别在于,它为每个"Async"方法提供了加载进度条。以下代码片段展示了当纹理加载时我们如何增加加载进度条:
@Override
public void onCreateResourcesAsync(IProgressListener pProgressListener)
throws Exception {
// Load texture number one
pProgressListener.onProgressChanged(10);
// Load texture number two
pProgressListener.onProgressChanged(20);
// Load texture number three
pProgressListener.onProgressChanged(30);
// We can continue to set progress to whichever value we'd like
// for each additional step through onCreateResourcesAsync...
}
提示
下载示例代码
你可以从你在www.PacktPub.com
的账户中下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.PacktPub.com/support
注册,我们会将文件直接通过电子邮件发送给你。
选择我们的引擎类型
在我们开始编程游戏之前,最好先确定游戏所需的性能需求。AndEngine 包含几种不同类型的引擎供我们选择使用,每种都有其自身的优势。当然,这些优势取决于我们计划创建的游戏类型。
准备工作
执行本章中的了解生命周期食谱,以在我们的 IDE 中设置一个基本的 AndEngine 项目,然后继续到如何操作…部分。
如何操作…
为了正确地为我们的游戏定义一个特定的Engine
对象,我们必须重写onCreateEngine()
方法,这是 AndEngine 启动过程的一部分。在任意基础的 AndEngine 活动中添加以下代码,以手动处理引擎的创建:
/* The onCreateEngine method allows us to return a 'customized' Engine object
* to the Activity which for the most part affects the way frame updates are
* handled. Depending on the Engine object used, the overall feel of the
* gameplay can alter drastically.
*/
@Override
public Engine onCreateEngine(EngineOptions pEngineOptions) {
return super.onCreateEngine(pEngineOptions);
/* The returned super method above simply calls:
return new Engine(pEngineOptions);
*/
}
工作原理…
以下是 AndEngine 中可用的各种Engine
对象的概览,以及一个简短的代码片段,展示如何设置每个Engine
对象:
-
Engine
:首先,我们有一个普通的Engine
对象。对于大多数游戏开发来说,Engine
对象并不理想,因为它在每秒帧数上没有任何限制。在两个不同的设备上,你很可能会注意到游戏速度的差异。一个思考方式是,如果两个不同的设备同时开始观看同一个视频,较快的设备可能会先完成视频观看,而不是同时完成。因此,在运行较慢的设备上可能会出现明显的问题,尤其是在物理是游戏重要部分的情况下。将这种类型的引擎集成到我们的游戏中不需要额外的步骤。 -
FixedStepEngine
:我们可用的第二种引擎是FixedStepEngine
。这是游戏开发中理想的引擎,因为它强制游戏循环以恒定速度更新,而与设备无关。这是通过根据经过的时间更新游戏,而不是根据设备执行代码的能力来实现的。FixedStepEngine
要求我们按顺序传递EngineOptions
对象和一个int
值。这个int
值定义了每秒引擎将强制运行的步数。以下代码创建了一个以恒定60
步每秒运行的引擎:@Override public Engine onCreateEngine(EngineOptions pEngineOptions) { // Create a fixed step engine updating at 60 steps per second return new FixedStepEngine(pEngineOptions, 60); }
-
LimitedFPSEngine
:LimitedFPSEngine
引擎允许我们设置引擎运行的每秒帧数限制。这将导致引擎进行一些内部计算,如果首选 FPS 与引擎当前实现的 FPS 之间的差值大于预设值,引擎将会等待一小段时间后再进行下一次更新。LimitedFPSEngine
在构造函数中需要两个参数,包括EngineOptions
对象和一个指定最大每秒帧数的int
值。以下代码创建了一个最大以 60 帧每秒运行的引擎:@Override public Engine onCreateEngine(EngineOptions pEngineOptions) { // Create a limited FPS engine, which will run at a maximum of 60 FPS return new LimitedFPSEngine(pEngineOptions, 60); }
-
SingleSceneSplitScreenEngine
和DoubleSceneSplitScreenEngine
:SingleSceneSplitScreenEngine
引擎和DoubleSceneSplitScreenEngine
引擎允许我们创建带有两个独立摄像头的游戏,可以是单个场景,通常用于单人游戏,也可以是两个场景,用于单个设备上的多人游戏。这些只是示例,然而,这两个引擎可以具有广泛的应用,包括迷你地图、多重视角、菜单系统等等。更多关于设置这些类型Engine
对象的详细信息,请参见第四章,创建分屏游戏。
选择分辨率策略
选择分辨率策略可能是一个敏感的话题,特别是考虑到我们正在处理的平台目前主要运行在从 3 英寸显示屏到 10.1 英寸的设备上。通常,开发者和用户都希望游戏能够占据设备显示的完整宽度和高度,但在某些情况下,我们可能需要仔细选择分辨率策略,以便按照我们开发者的意愿正确显示场景。在本节中,我们将讨论 AndEngine 中包含的各种分辨率策略,这将帮助我们决定哪种策略可能最适合我们应用程序的需求。
如何操作…
我们选择遵循的分辨率策略必须作为参数包含在EngineOptions
构造函数中,该函数是在 AndEngine 生命周期中的onCreateEngineOptions()
方法里创建的。以下代码使用FillResolutionPolicy
类创建我们的EngineOptions
对象,这一部分将在本章后面进行解释:
EngineOptions engineOptions = new EngineOptions(true,
ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(),
mCamera);
我们只需向构造函数传递另一个分辨率策略类变体,就可以选择不同的分辨率策略。
它的工作原理…
以下是 AndEngine 的BaseResolutionPolicy
子类型的概述。这些策略用于指定 AndEngine 如何根据各种因素处理应用程序的显示宽度和高度:
-
FillResolutionPolicy
:如果我们只是希望应用程序占据显示器的全部宽度和高度,FillResolutionPolicy
类是典型的分辨率策略。虽然此策略允许应用程序以真正的全屏模式运行,但它可能会导致场景为了占据显示器的全部可用尺寸而在某些部分产生明显的拉伸。我们只需在EngineOptions
构造函数中的分辨率策略参数中包含new FillResolutionPolicy()
,即可选择此分辨率策略。 -
FixedResolutionPolicy
:FixedResolutionPolicy
类允许我们为应用程序应用固定的显示尺寸,无论设备显示尺寸或Camera
对象尺寸如何。此策略可以通过new FixedResolutionPolicy(pWidth, pHeight)
传递给EngineOptions
,其中pWidth
定义了应用程序视图将覆盖的最终宽度,而pHeight
定义了应用程序视图将覆盖的最终高度。例如,如果我们向此策略类型的构造函数传递 800 的宽度和 480 的高度,在一个分辨率为 1280 x 752 的平板电脑上,由于分辨率策略与实际显示尺寸之间没有补偿,我们将得到一个空白黑色区域。 -
RatioResolutionPolicy
:如果需要在不扭曲精灵的情况下获得最大显示尺寸,RatioResolutionPolicy
类是最佳的分辨率策略选择。另一方面,由于 Android 设备范围广泛,涵盖了许多显示尺寸,某些设备可能会在显示的顶部和底部,或左右两侧看到“黑边”。此分辨率策略的构造函数可以传递一个float
值,用于定义显示尺寸的首选比率值,或者传递宽度和高度参数,从中通过宽度除以高度来提取比率值。例如,new RatioResolutionPolicy(1.6f)
来定义一个比率,或者new RatioResolutionPolicy(mCameraWidth, mCameraHeight)
,假设mCameraWidth
和mCameraHeight
是定义的Camera
对象尺寸。 -
RelativeResolutionPolicy
:这是最终的分辨率策略。该策略允许我们根据缩放因子对整个应用程序视图进行放大或缩小,1f
是默认值。我们可以使用构造函数对视图应用一般缩放——new RelativeResolutionPolicy(1.5f)
——这将使宽度和高度都增加1.5
倍;或者我们可以指定单独的宽度和高度缩放比例,例如,new RelativeResolutionPolicy(1.5f, 0.5f)
。需要注意的是,在使用此策略时,我们必须小心缩放因子,因为过大的缩放会导致应用程序在无警告的情况下关闭。尽量保持缩放因子小于1.8f
;否则,请确保在各种设备上进行大量测试。
创建对象工厂
对象工厂是在编程的各个领域中都有使用的有用设计模式。特别是在游戏开发中,工厂可能被用来生成敌人对象、生成子弹对象、粒子效果、物品对象等等。实际上,AndEngine 在创建声音、音乐、纹理和字体等时也使用了工厂模式。在这个示例中,我们将了解如何创建一个对象工厂,并讨论如何在我们自己的项目中使用它们来简化对象创建。
准备工作
请参考代码包中名为ObjectFactory
的类。
如何操作…
在这个示例中,我们使用ObjectFactory
类作为我们轻松创建和返回BaseObject
类子类型的方式。然而,在实际项目中,工厂通常不会包含内部类。
-
在我们创建对象工厂之前,我们应该创建我们的基类以及至少几个扩展基类的子类型:
public static class BaseObject { /* The mX and mY variables have no real purpose in this recipe, however in * a real factory class, member variables might be used to define position, * color, scale, and more, of a sprite or other entity. */ private int mX; private int mY; // BaseObject constructor, all subtypes should define an mX and mY value on creation BaseObject(final int pX, final int pY){ this.mX = pX; this.mY = pY; } }
-
一旦我们拥有一个带有任意数量的子类型的基类,我们现在可以开始考虑实现工厂设计模式。
ObjectFactory
类包含处理创建并返回类型为LargeObject
和SmallObject
对象的方法:public class ObjectFactory { // Return a new LargeObject with the defined 'x' and 'y' member variables. public static LargeObject createLargeObject(final int pX, final int pY){ return new LargeObject(pX, pY); } // Return a new SmallObject with the defined 'x' and 'y' member variables. public static SmallObject createSmallObject(final int pX, final int pY){ return new SmallObject(pX, pY); } }
它的工作原理是…
在这个示例的第一步中,我们创建了一个BaseObject
类。这个类包括两个成员变量mX
和mY
,如果我们处理的是 AndEngine 实体,可以想象它们将定义设备显示上的位置。一旦我们设置好了基类,就可以开始创建基类的子类型。这个示例中的BaseObject
类有两个内部类扩展它,一个名为LargeObject
,另一个名为SmallObject
。对象工厂的工作是确定我们需要创建的基类的哪个子类型,以及定义对象的属性,或者在这个实例中是mX
和mY
成员变量。
在第二步中,我们将查看ObjectFactory
代码。这个类应该包含与工厂处理的具体对象类型相关的任何对象创建的变化。在这种情况下,两个独立的对象仅需要一个定义了mX
和mY
变量的变量。在现实世界中,我们可能会发现创建一个SpriteFactory
类很有帮助。这个类可能包含几种不同的方法,用于通过SpriteFactory.createSprite()
、SpriteFactory.createButtonSprite()
和SpriteFactory.createTiledSprite()
创建普通精灵、按钮精灵或平铺精灵。此外,这些方法可能还需要定义位置、缩放、纹理区域、颜色等参数。这个类最重要的方面是它的方法返回一个对象的新子类型,因为这是工厂类背后的整个目的。
创建游戏管理器
游戏管理器是大多数游戏的重要组成部分。游戏管理器是一个类,应该包含与游戏玩法相关的数据;包括但不限于跟踪分数、信用/货币、玩家健康和其他一般游戏信息。在本主题中,我们将研究一个游戏管理器类,以了解它们如何融入我们的游戏结构。
准备就绪
请参考代码包中名为GameManager
的类。
如何操作…
我们将要介绍的游戏管理器将遵循单例设计模式。这意味着在整个应用程序生命周期中,我们只创建类的单个实例,并且可以在整个项目中访问其方法。按照以下步骤操作:
-
创建游戏管理器单例:
private static GameManager INSTANCE; // The constructor does not do anything for this singleton GameManager(){ } public static GameManager getInstance(){ if(INSTANCE == null){ INSTANCE = new GameManager(); } return INSTANCE; }
-
创建成员变量以及相应的获取器和设置器,以跟踪游戏数据:
// get the current score public int getCurrentScore(){ return this.mCurrentScore; } // get the bird count public int getBirdCount(){ return this.mBirdCount; } // increase the current score, most likely when an enemy is destroyed public void incrementScore(int pIncrementBy){ mCurrentScore += pIncrementBy; } // Any time a bird is launched, we decrement our bird count public void decrementBirdCount(){ mBirdCount -= 1; }
-
创建一个重置方法,将所有数据恢复到它们的初始值:
// Resetting the game simply means we must revert back to initial values. public void resetGame(){ this.mCurrentScore = GameManager.INITIAL_SCORE; this.mBirdCount = GameManager.INITIAL_BIRD_COUNT; this.mEnemyCount = GameManager.INITIAL_ENEMY_COUNT; }
它是如何工作的…
根据创建的游戏类型,游戏管理器肯定有不同的任务。这个示例的GameManager
类旨在模仿某个情感鸟品牌的类。我们可以看到,这个特定GameManager
类中的任务有限,但随着游戏玩法的复杂化,游戏管理器通常会增长,因为它需要跟踪更多信息。
在这个配方的第一步中,我们将GameManager
类设置为单例模式。单例是一种设计模式,旨在确保整个应用程序生命周期中只存在一个静态的此类实例。由于其静态特性,我们可以全局调用游戏管理器的方法,这意味着我们可以在项目中任何类中访问其方法,而无需创建新的GameManager
类。为了获取GameManager
类的实例,我们可以在项目的任何类中调用GameManager.getInstance()
。这样做将会在GameManager
类尚未被引用的情况下,为其分配一个新的GameManager
类给INSTANCE
。然后返回INSTANCE
对象,这样我们就可以调用GameManager
类中的数据修改方法,例如GameManager.getInstance().getCurrentScore()
。
在第二步中,我们创建了用于修改和获取存储在GameManager
类中的数据的 getter 和 setter 方法。这个配方中的GameManager
类包含三个int
值,用于跟踪重要的游戏数据:mCurrentScore
(当前得分)、mBirdCount
(鸟类计数)和mEnemyCount
(敌人计数)。这些变量各自都有对应的 getter 和 setter,使我们能够轻松地修改游戏数据。在游戏过程中,如果有一个敌人被摧毁,我们可以调用GameManager.getInstance().decrementEnemyCount()
以及GameManager.getInstance().incrementScore(pValue)
,其中pValue
可能由被摧毁的敌人对象提供。
设置这个游戏管理器的最后一步是提供一个重置游戏数据的方法。由于我们使用的是单例模式,无论我们是从小游戏转到主菜单、商店还是其他任何场景,GameManager
类的数据都不会自动恢复到默认值。这意味着每次重置关卡时,我们也必须重置游戏管理器的数据。在GameManager
类中,我们设置了一个名为resetGame()
的方法,其作用是简单地将数据恢复到原始值。
当开始一个新关卡时,我们可以调用GameManager.getInstance().resetGame()
以快速将所有数据恢复到初始值。然而,这是一个通用的GameManager
类,具体哪些数据应该在关卡重置或加载时重置完全由开发者决定。如果GameManager
类存储了信用/货币数据,例如在商店中使用时,最好不要将这个特定变量重置回默认值。
引入声音和音乐。
声音和音乐在游戏玩法中对用户起着重要作用。如果使用得当,它们可以给游戏带来额外的优势,让玩家在玩游戏时能够完全沉浸其中。另一方面,如果使用不当,它们也可能引起烦恼和不满。在这个配方中,我们将深入探讨 AndEngine 中的Sound
和Music
对象,涵盖从加载它们到修改它们的速率等内容。
准备工作
完成本章提供的了解生命周期配方,以便我们在 IDE 中设置一个基本的 AndEngine 项目。此外,我们应在项目的assets/
文件夹中创建一个新的子文件夹。将此文件夹命名为sfx
,并添加一个名为sound.mp3
的声音文件,以及另一个名为music.mp3
的文件。完成这些操作后,继续阅读如何操作…部分。
如何操作…
执行以下步骤,设置游戏以使用Sound
和Music
对象。请注意,Sound
对象用于声音效果,例如爆炸、碰撞或其他短音频播放事件。而Music
对象用于长时间音频播放事件,如循环菜单音乐或游戏音乐。
-
第一步是确保我们的
Engine
对象认识到我们计划在游戏中使用Sound
和Music
对象。在创建EngineOptions
对象之后,在我们的活动生命周期的onCreateEngineOptions()
方法中添加以下几行:engineOptions.getAudioOptions().setNeedsMusic(true); engineOptions.getAudioOptions().setNeedsSound(true);
-
在第二步中,我们将为声音和音乐工厂设置资源路径,然后加载
Sound
和Music
对象。Sound
和Music
对象是资源,所以你可能已经猜到,以下代码可以放入我们活动生命周期的onCreateResources()
方法中:/* Set the base path for our SoundFactory and MusicFactory to * define where they will look for audio files. */ SoundFactory.setAssetBasePath("sfx/"); MusicFactory.setAssetBasePath("sfx/"); // Load our "sound.mp3" file into a Sound object try { Sound mSound = SoundFactory.createSoundFromAsset(getSoundManager(), this, "sound.mp3"); } catch (IOException e) { e.printStackTrace(); } // Load our "music.mp3" file into a music object try { Music mMusic = MusicFactory.createMusicFromAsset(getMusicManager(), this, "music.mp3"); } catch (IOException e) { e.printStackTrace(); }
-
一旦
Sound
对象被加载到SoundManager
类中,我们就可以根据需要通过调用play()
来播放它们,无论是碰撞时、按钮点击还是其他情况:// Play the mSound object mSound.play();
-
Music
对象应该与Sound
对象以不同的方式处理。在大多数情况下,如果我们的Music
对象应该在游戏中持续循环,我们应在活动生命周期内处理所有的play()
和pause()
方法:/* Music objects which loop continuously should be played in * onResumeGame() of the activity life cycle */ @Override public synchronized void onResumeGame() { if(mMusic != null && !mMusic.isPlaying()){ mMusic.play(); } super.onResumeGame(); } /* Music objects which loop continuously should be paused in * onPauseGame() of the activity life cycle */ @Override public synchronized void onPauseGame() { if(mMusic != null && mMusic.isPlaying()){ mMusic.pause(); } super.onPauseGame(); }
工作原理…
在这个配方的第一步,我们需要让引擎知道我们是否将利用 AndEngine 播放Sound
或Music
对象的能力。如果忽略这一步,将导致应用程序出现错误,因此在我们将音频实现到游戏中之前,请确保在onCreateEngineOptions()
方法中返回EngineOptions
之前完成这一步。
在第二步中,我们访问应用程序生命周期的onCreateResources()
方法。首先,我们设置了SoundFactory
和MusicFactory
的基路径。如准备就绪部分所述,我们应在项目的assets/sfx
文件夹中为我们的音频文件保留一个文件夹,其中包含所有音频文件。通过在两个用于音频的工厂类上调用setAssetBasePath("sfx/")
,我们现在指向了查找音频文件的正确文件夹。完成此操作后,我们可以通过使用SoundFactory
类加载Sound
对象,以及通过使用MusicFactory
类加载Music
对象。Sound
和Music
对象要求我们传递以下参数:根据我们正在加载的音频对象类型选择mEngine.getSoundManager()
或mEngine.getMusicManager()
,Context
类即BaseGameActivity
,或者是这个活动,以及音频文件名称的字符串格式。
在第三步中,我们现在可以对希望播放的音频对象调用play()
方法。但是,这个方法应该在onCreateResources()
回调通知所有资源都已加载之后才能调用。为了安全起见,我们只需在 AndEngine 生命周期的onCreateResources()
部分之后,不再播放任何Sound
或Music
对象。
在最后一步中,我们设置Music
对象,以便在活动启动时以及从生命周期中调用onResumeGame()
时调用其play()
方法。在另一端,在onPauseGame()
期间,调用Music
对象的pause()
方法。在大多数情况下,最好以这种方式设置我们的Music
对象,特别是由于应用程序中断的最终不可避免性,例如电话或意外弹出点击。这种方法将允许我们的Music
对象在应用程序失去焦点时自动暂停,并在我们从最小化返回后重新开始执行。
注意事项
在这个配方和其他与资源加载相关的配方中,文件名已经被硬编码到代码片段中。这样做是为了增加简单性,但建议使用我们项目的strings.xml
Android 资源文件,以保持字符串的组织和易于管理。
还有更多…
AndEngine 使用 Android 原生的声音类为我们的游戏提供音频娱乐。除了play()
和pause()
方法之外,这些类还包含一些额外的方法,允许我们在运行时对音频对象有更多的控制。
音乐对象
以下列表包括为Music
对象提供的方法:
-
seekTo
:seekTo(pMilliseconds)
方法允许我们定义特定Music
对象的音频播放应从哪里开始。pMilliseconds
等于音频轨道的位置(毫秒),我们希望在调用Music
对象的play()
时从此位置开始播放。为了获取Music
对象的持续时间(毫秒),我们可以调用mMusic.getMediaPlayer().getDuration()
。 -
setLooping
:setLooping(pBoolean)
方法简单定义了Music
对象在到达持续时间末端后是否应从开始处重新播放。如果setLooping(true)
,则Music
对象会持续重复,直到应用程序关闭或调用setLooping(false)
为止。 -
setOnCompletionListener
:此方法允许我们在Music
对象中应用一个监听器,这给了我们待音频完成时执行函数的机会。这是通过向我们的Music
对象添加OnCompletionListener
来完成的,如下所示:mMusic.setOnCompletionListener(new OnCompletionListener(){ /* In the event that a Music object reaches the end of its duration, * the following method will be called */ @Override public void onCompletion(MediaPlayer mp) { // Do something pending Music completion } });
-
setVolume
:使用setVolume(pLeftVolume, pRightVolume)
方法,我们可以独立调整左和右立体声通道。音量控制的最低和最高范围等于0.0f
(无音量)和1.0f
(全音量)。
Sound 对象
以下列表包括为Sound
对象提供的方法:
-
setLooping
:具体详情请参阅上文Music
对象的setLooping
方法的描述。此外,Sound
对象允许我们使用mSound.setLoopCount(pLoopCount)
设置音频轨道循环的次数,其中pLoopCount
是一个定义循环次数的int
值。 -
setRate
:setRate(pRate)
方法允许我们定义Sound
对象的播放速率或速度,其中pRate
等于浮点值表示的速率。默认速率为1.0f
,降低速率会降低音频音调,提高速率会增加音频音调。请注意,Android API 文档指出,速率接受的范围在0.5f
至2.0f
之间。超出此范围可能会在播放时产生错误。 -
setVolume
:具体详情请参阅上文Music
对象的setVolume
方法的描述。
注意
对于那些不擅长音频创作的我们来说,有许多免费资源可供使用。网上有许多免费的音频数据库,我们可以在公共项目中使用,例如www.soundjay.com
。请注意,大多数免费使用的数据库要求对使用的文件进行署名。
处理不同类型的纹理
了解如何管理纹理应该是每位游戏开发者的主要优先任务之一。当然,仅了解纹理的基础知识也是可以制作游戏的,但长远来看,这很可能会导致性能问题、纹理溢出和其他不希望出现的结果。在本教程中,我们将探讨如何将纹理构建到游戏中,以提供效率,同时减少纹理填充问题出现的可能性。
准备工作
执行本章中给出的了解生命周期教程,以便我们在 IDE 中设置了一个基本的 AndEngine 项目。此外,此教程需要三个 PNG 格式的图像。第一个矩形命名为rectangle_one.png
,宽 30 像素,高 40 像素。第二个矩形命名为rectangle_two.png
,宽 40 像素,高 30 像素。最后一个矩形命名为rectangle_three.png
,宽 70 像素,高 50 像素。将这些矩形图像添加到项目的assets/gfx/
文件夹后,继续进行如何操作…部分。
如何操作…
在 AndEngine 中构建纹理时涉及两个主要组成部分。在以下步骤中,我们将创建一个所谓的纹理图集,它将存储在准备工作部分提到的三个矩形 PNG 图像中的三个纹理区域。
-
此步骤是可选的。我们将
BitmapTextureAtlasTextureRegionFactory
类指向我们的图像所在的文件夹。默认情况下,工厂指向assets/
文件夹。通过在工厂的默认基本路径后附加gfx/
,现在它将在assets/gfx/
中查找我们的图像:BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
-
接下来,我们将创建
BitmapTextureAtlas
。纹理图集可以看作是包含许多不同纹理的地图。在这种情况下,我们的“地图”或BitmapTextureAtlas
的大小将为 120 x 120 像素:// Create the texture atlas at a size of 120x120 pixels BitmapTextureAtlas mBitmapTextureAtlas = new BitmapTextureAtlas(mEngine.getTextureManager(), 120, 120);
-
当我们有了
BitmapTextureAtlas
可以使用时,现在可以创建我们的ITextureRegion
对象,并将它们放置在BitmapTextureAtlas
纹理中的特定位置。我们将使用BitmapTextureAtlasTextureRegionFactory
类,它帮助我们绑定 PNG 图像到特定的ITextureRegion
对象,并在我们上一步创建的BitmapTextureAtlas
纹理图集中定义一个位置来放置ITextureRegion
对象:/* Create rectangle one at position (10, 10) on the mBitmapTextureAtlas */ ITextureRegion mRectangleOneTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_one.png", 10, 10); /* Create rectangle two at position (50, 10) on the mBitmapTextureAtlas */ ITextureRegion mRectangleTwoTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_two.png", 50, 10); /* Create rectangle three at position (10, 60) on the mBitmapTextureAtlas */ ITextureRegion mRectangleThreeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "rectangle_three.png", 10, 60);
-
最后一步是将我们的
ITextureRegion
对象加载到内存中。我们可以通过调用包含所述ITextureRegion
对象的BitmapTextureAtlas
图集来实现这一点:mBitmapTextureAtlas.load();
工作原理…
在 AndEngine 开发中,为了给我们的项目创建纹理,我们将使用两个主要组件。第一个组件被称为BitmapTextureAtlas
,可以将其视为一个具有最大宽度和高度的平面,可以在其宽度和高度范围内存储子纹理。这些子纹理被称为纹理区域,或者具体到 AndEngine 中是ITextureRegion
对象。ITextureRegion
对象的目的仅是作为对内存中特定纹理的引用,该纹理位于BitmapTextureAtlas
图集中的 x 和 y 位置。看待这两个组件的一种方式是想象一块空白的画布,这代表纹理图集,以及一把贴纸,这些将代表纹理区域。画布会有一个最大尺寸,在这个区域内我们可以将贴纸放在任何我们想要的地方。有了这个想法,我们在画布上放置了一把贴纸。现在,我们所有的贴纸都整齐地摆放在这个画布上,我们可以随时取用并放置到任何我们想要的地方。还有一些更细节的内容,但这会在稍后介绍。
了解了BitmapTextureAtlas
和ITextureRegion
对象的基础知识之后,创建我们纹理的步骤现在应该更有意义了。如第一步所述,设置BitmapTextureAtlasTextureRegionFactory
类的基路径是完全可选的。我们包含这一步只是因为它让我们无需在创建ITextureRegion
对象时重复说明我们的图像位于哪个文件夹。例如,如果我们不设置基路径,我们就必须以gfx/rectangle_one.png
、gfx/rectangle_two.png
等方式引用我们的图像。
在第二步中,我们创建BitmapTextureAtlas
对象。这一步相当直接,因为我们只需指定引擎的TextureManager
对象来处理纹理加载,以及纹理图集的宽度和高度,按此顺序。由于在这些步骤中我们只处理三个小图像,120x120 像素就非常合适。
关于纹理图集,有一点非常重要,那就是永远不要创建过多的纹理图集;比如,不要为了存放一个 32x32 像素的单个图像而创建一个 256x256 的图集。另一个重要点是,避免创建超过 1024x1024 像素的纹理图集。安卓设备在最大纹理尺寸上各不相同,尽管有些设备可能能够存储高达 2048x2048 像素的纹理,但大量设备的最大限制是 1024x1024。超过最大纹理尺寸将会导致在启动时强制关闭,或者在特定设备上无法正确显示纹理。如果没有其他选择,确实需要大图像,请参考第四章中的背景拼接部分,使用摄像头。
在这个食谱的第三步中,我们正在创建我们的ITextureRegion
对象。换句话说,我们正在将指定的图像应用到mBitmapTextureAtlas
对象上,并定义该图像在图集中的确切位置。使用BitmapTextureAtlasTextureRegionFactory
类,我们可以调用createFromAsset(pBitmapTextureAtlas, pContext, pAssetPath, pTextureX, pTextureY)
方法,这使得创建纹理区域变得轻而易举。从左到右列出参数的顺序,pBitmapTextureAtlas
参数指定了希望存储ITextureRegion
对象的纹理图集。pContext
参数允许类从gfx/
文件夹中打开图像。pAssetPath
参数定义了我们正在寻找的特定文件的名称,例如rectangle_one.png
。最后的两个参数,pTextureX
和pTextureY
,定义了放置ITextureRegion
对象的纹理图集上的位置。以下图像表示在第三步中定义的三个ITextureRegion
对象的样子。请注意,代码和图像之间的位置是一致的:
在前一个图像中,请注意,每个矩形与纹理边缘之间至少有 10 个像素的间隔。ITextureRegion
对象并不是像这样间隔开来以使事物更易于理解,尽管这样做有帮助。实际上,它们是间隔开来的,以便添加所谓的纹理图集源间隔。这种间隔的作用是防止在将纹理应用到精灵时发生纹理重叠。这种重叠被称为纹理溢出。尽管按照本食谱创建的纹理并不能完全消除纹理溢出的可能性,但在将某些纹理选项应用于纹理图集时,它确实降低了这个问题发生的可能性。
想了解更多关于纹理选项的信息,请参阅本章中提供的应用纹理选项食谱。此外,本主题中的还有更多...部分描述了创建纹理图集的另一种方法,这种方法完全解决了纹理溢出的问题!强烈推荐。
还有更多内容…
当涉及到将纹理添加到我们的游戏时,我们可以采取多种不同的方法。它们都有自己的优点,有些甚至涉及到负面因素。
BuildableBitmapTextureAtlas
BuildableBitmapTextureAtlas
对象是一种将ITextureRegion
对象实现到我们的纹理图集中的便捷方式,无需手动定义位置。BuildableBitmapTextureAtlas
纹理图集的目的是通过将它们放置到最方便的坐标上来自动放置其ITextureRegion
对象。这种创建纹理的方法是最简单且最高效的,因为当构建包含许多纹理图集的大型游戏时,这种方法可能会节省时间,有时甚至可以避免错误。除了BuildableBitmapTextureAtlas
的自动化之外,它还允许开发者定义纹理图集源的透明边距,从而消除纹理溢出的任何情况。这是 AndEngine 的 GLES 1.0 分支中最突出的视觉问题之一,因为当时没有内置方法为纹理图集提供边距。
使用BuildableBitmapTextureAtlas
图集与BitmapTextureAtlas
路径略有不同。以下是使用BuildableBitmapTextureAtlas
图集的此食谱代码:
/* Create a buildable bitmap texture atlas - same parameters required
* as with the original bitmap texture atlas */
BuildableBitmapTextureAtlas mBuildableBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 120, 120);
/* Create the three ITextureRegion objects. Notice that when using
* the BuildableBitmapTextureAtlas, we do not need to include the final
* two pTextureX and pTextureY parameters. These are handled automatically! */
ITextureRegion mRectangleOneTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_one.png");
ITextureRegion mRectangleTwoTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_two.png");
ITextureRegion mRectangleThreeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBuildableBitmapTextureAtlas, this, "rectangle_three.png");
// Buildable bitmap texture atlases require a try/catch statement
try {
/* Build the mBuildableBitmapTextureAtlas, supplying a BlackPawnTextureAtlasBuilder
* as its only parameter. Within the BlackPawnTextureAtlasBuilder's parameters, we
* provide 1 pixel in texture atlas source space and 1 pixel for texture atlas source
* padding. This will alleviate the chance of texture bleeding.
*/
mBuildableBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1));
} catch (TextureAtlasBuilderException e) {
e.printStackTrace();
}
// Once the atlas has been built, we can now load
mBuildableBitmapTextureAtlas.load();
如此代码所示,BuildableBitmapTextureAtlas
与BitmapTextureAtlas
图集之间存在一些细微差别。首先要注意的是,在创建我们的ITextureRegion
对象时,我们不再需要指定纹理区域应在纹理图集上的放置位置。使用BuildableBitmapTextureAtlas
替代方案时的第二个小变化是,在调用load()
方法之前,我们必须在mBuildableBitmapTextureAtlas
上调用build(pTextureAtlasBuilder)
方法。在build(pTextureAtlasBuilder)
方法中,我们必须提供一个BlackPawnTextureAtlasBuilder
类,定义三个参数。按顺序,这些参数是pTextureAtlasBorderSpacing
、pTextureAtlasSourceSpacing
和pTextureAtlasSourcePadding
。在上述代码片段中,我们几乎可以消除所有情况下的纹理溢出可能性。然而,在极端情况下,如果仍有纹理溢出,只需增加第三个参数,这将有助于解决任何问题。
纹理区域块
纹理区域块本质上与普通纹理区域是相同的对象。两者的区别在于,纹理区域块允许我们传递一个图像文件并从中创建一个精灵表。这是通过指定我们精灵表中的列数和行数来完成的。从此,AndEngine 将自动将纹理区域块均匀分布成段。这将允许我们在TiledTextureRegion
对象中导航每个段。这就是纹理区域块如何表现为创建具有动画的精灵的样子。
注意
实际的精灵表不应该在每列和每行周围有轮廓。在上一张图片中它们是为了显示如何将精灵表划分为等分段。
假设前面的图像宽度为 165 像素,高度为 50 像素。由于我们有 11 个单独的列和单行,我们可以像这样创建TiledTextureRegion
对象:
TiledTextureRegion mTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(mBitmapTextureAtlas, context,"sprite_sheet.png",11,1);
这段代码的作用是告诉 AndEngine 将sprite_sheet.png
图像划分为11
个独立的部分,每个部分宽度为 15 像素(因为 165 像素除以 11 个部分等于 15)。现在我们可以使用这个分块纹理区域对象实例化一个带有动画的精灵。
压缩纹理
除了更常见的图像类型(.bmp
、.jpeg
和.png
),AndEngine 还内置了对 PVR 和 ETC1 压缩纹理的支持。使用压缩纹理的主要好处是它对减少加载时间和可能在游戏过程中提高帧率的影响。就此而言,使用压缩纹理也有缺点。例如,ETC1 不支持在其纹理中使用 alpha 通道。压缩纹理也可能导致纹理质量明显下降。这些类型纹理的使用应与压缩纹理所表示的对象的重要性相关。你很可能不希望将整个游戏的纹理格式基于压缩纹理,但对于大量微妙的图像,使用压缩纹理可以为你的游戏带来明显的性能提升。
另请参阅
-
本章中的创建资源管理器。
-
本章中的应用纹理选项。
应用纹理选项
我们已经讨论了 AndEngine 提供的不同类型的纹理;现在让我们看看我们可以为纹理提供哪些选项。这个主题的内容往往会对我们游戏的质量和性能产生显著影响。
准备就绪
执行本章中提供的处理不同类型的纹理的步骤,以便我们使用BitmapTextureAtlas
或BuildableBitmapTextureAtlas
加载,设置了一个基本的 AndEngine 项目。
如何操作…
为了修改纹理图集的选项和/或格式,我们需要根据是否要定义选项、格式或两者都定义,向BitmapTextureAtlas
构造函数中添加一个或两个参数。以下是修改纹理格式和纹理选项的代码:
BitmapTextureAtlas mBitmapTextureAtlas = new BitmapTextureAtlas(mEngine.getTextureManager(), 1024, 1024, BitmapTextureFormat.RGB_565, TextureOptions.BILINEAR);
从这里开始,放置在此特定纹理图集中的所有纹理区域都将应用定义的纹理格式和选项。
工作原理…
AndEngine 允许我们为纹理图集应用纹理选项和格式。应用于纹理图集的各种选项和格式的组合将影响精灵对我们游戏的整体质量和性能影响。当然,如果提到的精灵使用了与修改后的BitmapTextureAtlas
图集相关的ITextureRegion
对象,情况也是如此。
AndEngine 中可用的基本纹理选项如下:
-
最近邻:此纹理选项默认应用于纹理图集。这是我们能够应用在纹理图集中的最快性能的纹理选项,但也是质量最差的。这个选项意味着纹理将通过获取与像素最近的纹理元素颜色来应用构成显示的像素的混合。类似于像素代表数字图像的最小元素,纹理元素(texel)代表纹理的最小元素。
-
双线性:AndEngine 中的第二个主要的纹理过滤选项称为双线性纹理过滤。这种方法在性能上会有所下降,但缩放后精灵的质量将提高。双线性过滤获取每个像素的四个最近的纹理元素,以提供更平滑的屏幕图像混合。
请查看以下图表,以比较双线性过滤和最近邻过滤:
这两张图像以最高的位图格式渲染。在这种情况下,最近邻与双线性过滤之间的区别非常明显。在图像的左侧,双线性星星几乎看不到锯齿边缘,颜色非常平滑。在右侧,我们得到了一个使用最近邻过滤渲染的星星。由于锯齿边缘更加明显,质量水平受到影响,如果仔细观察,颜色也不够平滑。
以下是几个额外的纹理选项:
重复:重复纹理选项允许精灵“重复”纹理,假设精灵的大小超出了ITextureRegion
对象的宽度和高度。在大多数游戏中,地形通常是通过创建重复纹理并拉伸精灵的大小来生成的,而不是创建许多独立的精灵来覆盖地面。
让我们看看如何创建一个重复纹理:
/* Create our repeating texture. Repeating textures require width/height which are a power of two */
BuildableBitmapTextureAtlas texture = new BuildableBitmapTextureAtlas(engine.getTextureManager(), 32, 32, TextureOptions.REPEATING_BILINEAR);
// Create our texture region - nothing new here
mSquareTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(texture, context, "square.png");
try {
// Repeating textures should not have padding
texture.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0));
texture.load();
} catch (TextureAtlasBuilderException e) {
Debug.e(e);
}
之前的代码基于一个 32 x 32 像素的方形图像。创建重复纹理时需要注意的两点是:
-
使用重复纹理选项格式的纹理图集需要尺寸为 2 的幂(2, 4, 8, 16 等)
-
如果你使用的是可构建的纹理图集,在
build()
方法中不要应用填充或间距,因为这在纹理的重复中会被考虑在内,破坏了重复纹理的第一个规则。
接下来,我们需要创建一个使用这种重复纹理的精灵:
/* Increase the texture region's size, allowing repeating textures to stretch up to 800x480 */
ResourceManager.getInstance().mSquareTextureRegion.setTextureSize(800, 480);
// Create a sprite which stretches across the full screen
Sprite sprite = new Sprite(0, 0, 800, 480, ResourceManager.getInstance().mSquareTextureRegion, mEngine.getVertexBufferObjectManager());
我们在这里所做的的是将纹理区域的尺寸增加到 800 x 480 像素。这并不会改变应用了重复选项的纹理图像的大小,而是允许图像最多重复至 800 x 480 像素。这意味着,如果我们创建了一个精灵并提供了重复纹理,我们可以将精灵的尺寸缩放到 800 x 480 像素,同时仍然显示重复效果。然而,如果精灵超出了纹理区域的宽度或高度尺寸,超出区域将不应用纹理。
这是来自设备截图的结果:
预乘透明度:最后,我们有一个选项可以将预乘透明度纹理选项添加到我们的纹理中。这个选项的作用是将每个 RGB 值乘以指定的透明通道,然后在最后应用透明通道。这个选项的主要目的是让我们能够修改颜色的不透明度而不会损失颜色。请记住,直接修改带有预乘透明度值的精灵的透明度值可能会产生不想要的效果。当这个选项应用于透明度为0
的精灵时,精灵可能不会完全透明。
当将纹理选项应用到我们的纹理图集时,我们可以选择最近邻或双线性纹理过滤选项。除了这些纹理过滤选项,我们还可以选择重复选项、预乘透明度选项,或者两者都选。
还有更多…
除了纹理选项,AndEngine 还允许我们设置每个纹理图集的纹理格式。纹理格式,类似于纹理选项,通常根据其用途来决定。纹理的格式可以极大地影响图像的性能和质量,甚至比纹理选项更明显。纹理格式允许我们选择纹理图集中 RGB 值的可用颜色范围。根据所使用的纹理格式,我们还可能允许或不允许精灵具有任何透明度值,这会影响纹理的透明度。
纹理格式的命名约定并不复杂。所有格式的名称类似于RGBA_8888,下划线左侧指的是纹理可用的颜色或透明通道。下划线右侧指的是每个颜色通道可用的位数。
纹理格式
以下是可以使用的纹理格式:
-
RGBA_8888
:允许纹理使用红色、绿色、蓝色和透明通道,每个通道分配 8 位。由于我们有 4 个通道,每个通道分配 8 位(4 x 8),我们得到一个 32 位的纹理格式。这是这四种格式中最慢的纹理格式。 -
RGBA_4444
:允许纹理使用红色、绿色、蓝色和透明通道,每个通道分配 4 位。按照与前一个格式相同的规则,我们得到一个 16 位的纹理格式。与RGBA_8888
相比,你会注意到这个格式的改进,因为我们保存的信息量只有 32 位格式的一半。质量将明显受损;请看以下图片:在这张图片中,我们比较了两种纹理格式的差异。两颗星星都使用默认的纹理选项(最近邻)进行渲染,这与图像的 RGBA 格式无关。我们更感兴趣的是两颗星星的颜色质量。左侧的星星以全 32 位颜色能力进行渲染,右侧的则是 16 位。两颗星星之间的差异相当明显。
-
RGB_565
:这是另一种 16 位的纹理格式,不过它不包括透明通道;使用这种纹理格式的纹理将不支持透明度。由于缺乏透明度,这种格式的需求有限,但它仍然很有价值。这种纹理格式的一个使用场景是显示全屏图像,如背景。背景不需要透明度,因此在引入背景时,记住这种格式是明智的。这样节省的性能相当明显。提示
RGB_565
格式的颜色质量与之前展示的RGBA_4444
星形图像大致相同。 -
A_8
:最后,我们来看最后一种纹理格式,它是 8 位的透明通道(不支持颜色)。这也是一种使用范围有限的格式;A_8 格式通常用作具有颜色的精灵的透明遮罩(叠加)。这种格式的一个使用例子是,通过简单地叠加这种纹理的精灵,然后随着时间的推移改变透明度,使屏幕渐变出现或消失。
在创建纹理图集时,考虑哪些类型的精灵将使用哪种类型的纹理区域,并据此将它们打包到纹理图集中是一个好主意。对于较重要的精灵,我们很可能会选择使用RGBA_8888
纹理格式,因为这些精灵将是我们游戏的主要焦点。这些对象可能包括前景精灵、主角精灵或屏幕上任何视觉上更突出的物体。背景覆盖了设备整个表面区域,所以我们很可能不需要透明度。对于这些精灵,我们将使用RGB_565
以移除透明通道,这将有助于提高性能。最后,我们有那些可能颜色不多、可能较小或只是不需要太多视觉吸引力的物体。对于这类精灵,我们可以使用RGBA_4444
纹理格式,以减少这些纹理所需的内存一半。
参见
-
本章了解生命周期。
-
本章介绍不同类型的纹理的处理方法。
-
在第二章中,使用实体,介绍了如何通过精灵使场景生动起来。
使用 AndEngine 字体资源
AndEngine 字体设置简单,可以包含在我们的Text
对象中使用,显示在屏幕上。我们可以选择预设字体,也可以通过assets
文件夹添加自己的字体。
准备就绪
执行本章提供的了解生命周期的步骤,这样我们就可以在 IDE 中设置基本的 AndEngine 项目,然后继续阅读如何操作…部分。
如何操作…
下面的代码片段展示了创建预设、自定义资源、预设描边和自定义资源描边字体对象的四种不同选项。字体创建应该在BaseGameActivity
类的onCreateResources()
方法中进行。
-
预设字体的
create()
方法如下:Font mFont = FontFactory.create(mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL), 32f, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT) mFont.load();
-
自定义字体的
createFromAsset()
方法如下:Font mFont = FontFactory.createFromAsset(mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, this.getAssets(), "Arial.ttf", 32f, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT); mFont.load();
-
描边字体的
createStroke()
和createStrokeFromAsset()
方法如下:BitmapTextureAtlas mFontTexture = new BitmapTextureAtlas(mEngine.getTextureManager(), 256, 256, TextureOptions.BILINEAR); Font mFont = FontFactory.createStroke(mEngine.getFontManager(), mFontTexture, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32, true, org.andengine.util.adt.color.Color.WHITE_ABGR_PACKED_INT, 3, org.andengine.util.adt.color.Color.BLACK_ABGR_PACKED_INT); mFont.load();
工作原理…
如我们所见,根据我们希望字体呈现的效果,我们可以采取不同的方法来创建我们的Font
对象。然而,所有字体都需要我们定义纹理宽度和纹理高度,无论是直接作为FontFactory
类create
方法的参数,还是通过使用BitmapTextureAtlas
对象间接定义。在之前的代码片段中,我们使用宽度为256
像素、高度为256
像素的纹理大小创建了所有三个Font
对象。不幸的是,目前还没有简单的方法在运行时自动确定所需的纹理大小,以支持不同的语言、文本大小、描边值或字体样式。
目前,最常见的方法是将纹理宽度和高度设置为大约256
像素,然后向上或向下进行小调整,直到纹理大小刚好合适,不会在Text
对象中产生伪影。字体大小在确定Font
对象所需的最终纹理大小中起着最重要的作用,因此非常大的字体,例如 32 及以上,可能需要更大的纹理大小。
注意
所有Font
对象在能够正确显示Text
对象中的字符之前,都需要调用load()
方法。
让我们看看如何操作…部分中介绍的各种方法是如何工作的:
-
create()
方法:create()
方法不允许太多自定义。从第五个参数开始,这个方法的参数包括提供字体样式、字体大小、抗锯齿选项和颜色。我们使用的是 Android 原生字体类,它只支持几种不同的字体和样式。 -
createFromAsset()
方法:我们可以使用这个方法将自定义字体引入到我们的项目中,通过我们的assets
文件夹。假设我们有一个叫做Arial.ttf
的真类型字体位于项目的assets
文件夹中。我们可以看到,一般的创建过程是相同的。在这个方法中,我们必须传递活动的AssetManager
类,这可以通过我们活动的getAssets()
方法获得。接下来的参数是我们想要导入的真类型字体。 -
createStroke()
和createStrokeFromAsset()
方法:最后,我们有了描边字体。描边字体使我们能够为Text
对象中的字符添加轮廓。在这些情况下,当我们希望我们的文本“突出”时,这些字体很有用。为了创建描边字体,我们需要提供一个纹理图集作为第二个参数,而不是传递引擎的纹理管理器。从这个点开始,我们可以通过字体类型或通过我们的assets
文件夹来创建描边字体。此外,我们还提供了定义两个新颜色值的选项,这两个值作为最后两个参数添加。有了这些新参数,我们能够调整轮廓的厚度以及颜色。
还有更多…
Font
类目前的设置,最好预加载我们期望通过Text
对象显示的字符。不幸的是,AndEngine 目前在还有新字母要绘制时仍然调用垃圾回收器,因此为了避免Text
对象首次“熟悉”字母时的卡顿,我们可以调用以下方法:
mFont.prepareLetters("abcdefghijklmnopqrstuvwxyz".toCharArray())
此方法调用将准备从 a 到 z 的小写字母。这个方法应该在游戏加载屏幕期间的某个时刻被调用,以避免任何可察觉的垃圾回收。在离开Font
对象的话题之前,还有一个重要的类我们应该讨论。AndEngine 包含一个名为FontUtils
的类,它允许我们通过measureText(pFont, pText)
方法获取关于Text
对象在屏幕上的宽度的信息。在处理动态变化的字符串时,这很重要,因为它为我们提供了重新定位Text
对象的选项,假设字符串的宽度或高度(以像素为单位)已经改变。
另请参阅
-
了解本章中的生命周期。
-
在本章中处理不同类型的纹理。
-
在第二章《使用实体》中,将文本应用到图层。
创建资源管理器
在本主题中,我们最终将从更大的角度查看我们的资源。有了资源管理器,我们将能够轻松地通过单一、方便的位置,调用如loadTextures()
、loadSounds()
或loadFonts()
等方法,来加载游戏需要的不同类型的资源。
准备就绪
请参考代码包中名为ResourceManager
的类。
如何操作…
ResourceManager
类是以单例设计模式为理念设计的。这允许我们通过简单的调用ResourceManager.getInstance()
来全局访问我们游戏的所有资源。ResourceManager
类的主要目的是存储资源对象,加载资源,以及卸载资源。以下步骤展示了我们如何使用ResourceManager
来处理我们游戏场景之一的纹理。
-
声明将在我们游戏的不同场景中使用的所有资源:
/* The variables listed should be kept public, allowing us easy access to them when creating new Sprites, Text objects and to play sound files */ public ITextureRegion mGameBackgroundTextureRegion; public ITextureRegion mMenuBackgroundTextureRegion; public Sound mSound; public Font mFont;
-
提供处理在
ResourceManager
类中声明的音频、图形和字体资源加载的load
方法:public synchronized void loadGameTextures(Engine pEngine, Context pContext){ // Set our game assets folder in "assets/gfx/game/" BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/game/"); BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(pEngine.getTextureManager(), 800, 480); mGameBackgroundTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, pContext, "game_background.png"); try { mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1)); mBitmapTextureAtlas.load(); } catch (TextureAtlasBuilderException e) { Debug.e(e); } }
-
第三步涉及提供一个与我们的
ResourceManager
类的load
方法相对应的所有资源的卸载方法:public synchronized void unloadGameTextures(){ // call unload to remove the corresponding texture atlas from memory BuildableBitmapTextureAtlas mBitmapTextureAtlas = (BuildableBitmapTextureAtlas) mGameBackgroundTextureRegion.getTexture(); mBitmapTextureAtlas.unload(); // ... Continue to unload all textures related to the 'Game' scene // Once all textures have been unloaded, attempt to invoke the Garbage Collector System.gc(); }
它是如何工作的…
通过在项目中实现一个ResourceManager
类,我们可以轻松地完全独立地加载各种场景资源。因此,我们必须确保我们的public
类方法是同步的,以确保我们在一个线程安全的环境中运行。这对于单例的使用尤为重要,因为我们只有一个类实例,有多个线程访问它的可能性。除此之外,现在我们只需要一行代码即可加载场景资源,这极大地帮助我们的主活动类保持更有条理。以下是使用资源管理器时,我们的onCreateResources()
方法应该看起来像什么样子:
@Override
public void onCreateResources(
OnCreateResourcesCallback pOnCreateResourcesCallback) {
// Load the game texture resources
ResourceManager.getInstance().loadGameTextures(mEngine, this);
// Load the font resources
ResourceManager.getInstance().loadFonts(mEngine);
// Load the sound resources
ResourceManager.getInstance().loadSounds(mEngine, this);
pOnCreateResourcesCallback.onCreateResourcesFinished();
}
在第一步中,我们声明了所有的资源,包括Font
对象,ITextureRegion
对象,以及Sound
/Music
对象。在这个特定的示例中,我们只处理有限数量的资源,但在一个功能齐全的游戏中,这个类可能包括 50、75,甚至超过 100 个资源。为了从我们的ResourceManager
类中获取资源,我们只需在任何类中包含以下代码行:
ResourceManager.getInstance().mGameBackgroundTextureRegion
。
在第二步中,我们创建了loadGameTextures(pEngine, pContext)
方法,用于加载Game
场景的纹理。对于游戏中的每个附加场景,我们应该有一个单独的load
方法。这使得可以轻松地动态加载资源。
在最后一步中,我们创建unload
方法,处理与每个load
方法相对应的资源卸载。然而,如果有任何数量的资源在我们的游戏多个场景中使用,可能需要创建一个没有伴随unload
方法的load
方法。
还有更多…
在较大的项目中,有时我们可能会发现自己频繁地将主要对象传递给类。资源管理器的另一个用途是存储一些更重要的游戏对象,如Engine
或Camera
。这样我们就不必不断地将这些对象作为参数传递,而可以调用相应的get
方法以获取游戏的Camera
、Engine
或我们将在类中引用的任何其他特定对象。
另请参阅
-
在本章中引入声音和音乐。
-
在本章中处理不同类型的纹理。
-
在本章中使用 AndEngine 字体资源。
保存和加载游戏数据
在游戏结构章节的最后一个主题中,我们将设置一个可以在项目中使用的类来管理和设置数据。我们必须保存的更明显的游戏数据应该包括角色状态、高分和其他可能在我们的游戏中包含的各种数据。我们还应该跟踪游戏可能具有的某些选项,例如用户是否静音、血腥效果等。在这个示例中,我们将使用一个名为SharedPreferences
的类,它将允许我们轻松地将数据保存到设备上,以便在稍后的时间检索。
注意
SharedPreferences
类是快速存储和检索原始数据类型的一种很好的方式。然而,随着数据量的增加,我们用来存储数据的方法的需求也会增加。如果我们的游戏确实需要存储大量数据,可以考虑使用 SQLite 数据库来存储数据。
准备工作
请参考代码包中名为UserData
的类。
如何操作…
在这个示例中,我们设置了一个名为UserData
的类,该类将存储一个布尔变量以决定是否静音,以及一个int
变量,该变量将定义用户已解锁的最高级别。根据游戏的需求,可能需要在类中包含更多或更少的数据类型,无论是最高分、货币还是其他与游戏相关的数据。以下步骤描述了如何设置一个类,在用户的设备上包含和存储用户数据:
-
第一步涉及声明我们的常量
String
变量,这些变量将保存对我们偏好文件的引用,以及保存对偏好文件内部数据引用的“键”名称,以及相应的“值”变量。此外,我们还声明了SharedPreferences
对象以及一个编辑器:// Include a 'filename' for our shared preferences private static final String PREFS_NAME = "GAME_USERDATA"; /* These keys will tell the shared preferences editor which data we're trying to access */ private static final String UNLOCKED_LEVEL_KEY = "unlockedLevels"; private static final String SOUND_KEY = "soundKey"; /* Create our shared preferences object & editor which will be used to save and load data */ private SharedPreferences mSettings; private SharedPreferences.Editor mEditor; // keep track of our max unlocked level private int mUnlockedLevels; // keep track of whether or not sound is enabled private boolean mSoundEnabled;
-
为我们的
SharedPreferences
文件创建一个初始化方法。这个方法将在我们的游戏首次启动时被调用,如果不存在,则为我们的游戏创建一个新文件,如果存在,则从偏好文件加载现有值:public synchronized void init(Context pContext) { if (mSettings == null) { /* Retrieve our shared preference file, or if it's not yet * created (first application execution) then create it now */ mSettings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); /* Define the editor, used to store data to our preference file */ mEditor = mSettings.edit(); /* Retrieve our current unlocked levels. if the UNLOCKED_LEVEL_KEY * does not currently exist in our shared preferences, we'll create * the data to unlock level 1 by default */ mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1); /* Same idea as above, except we'll set the sound boolean to true * if the setting does not currently exist */ mSoundEnabled = mSettings.getBoolean(SOUND_KEY, true); } }
-
接下来,我们将为那些打算存储在
SharedPreferences
文件中的每个值提供获取方法,以便我们可以在整个游戏中访问数据:/* retrieve the max unlocked level value */ public synchronized int getMaxUnlockedLevel() { return mUnlockedLevels; }
-
最后,我们必须为那些打算存储在
SharedPreferences
文件中的每个值提供设置方法。设置方法将负责将数据保存到设备上:public synchronized void unlockNextLevel() { // Increase the max level by 1 mUnlockedLevels++; /* Edit our shared preferences unlockedLevels key, setting its * value our new mUnlockedLevels value */ mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels); /* commit() must be called by the editor in order to save * changes made to the shared preference data */ mEditor.commit(); }
工作原理…
这个类展示了我们如何通过使用SharedPreferences
类轻松地存储和检索游戏的数据和选项。UserData
类的结构相当直接,可以以相同的方式使用,以便适应我们可能想要在游戏中包含的各种其他选项。
在第一步中,我们只是开始声明所有必要的常量和成员变量,这些变量我们将用于处理游戏中的不同类型的数据。对于常量,我们有一个名为PREFS_NAME
的String
变量,它定义了游戏的偏好文件的名称,还有另外两个String
变量,它们将分别作为对偏好文件中单个原始数据类型的引用。对于每个键常量,我们应该声明一个相应的变量,当数据第一次加载时,偏好文件数据将存储到这个变量中。
在第二步中,我们提供了从游戏的偏好文件中加载数据的方法。这个方法只需要在游戏启动过程中调用一次,以将SharedPreferences
文件中的数据加载到UserData
类的成员变量中。首先调用context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
,我们检查是否有针对我们的应用程序在PREFS_NAME
字符串下的SharedPreference
文件,如果没有,那么我们将创建一个新的文件——MODE_PRIVATE
,意味着该文件对其他应用程序不可见。
一旦完成,我们可以从偏好文件中调用获取器方法,如mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1)
。这将偏好文件中UNLOCKED_LEVEL_KEY
键的数据传递给mUnlockedLevels
。如果游戏的偏好文件当前没有为定义的键保存任何值,那么默认值1
将被传递给mUnlockedLevels
。这将针对UserData
类处理的每种数据类型继续进行。在这种情况下,只是关卡和声音。
在第三步中,我们设置了对UserData
类处理的每种数据类型相对应的获取器方法。这些方法可以在游戏中的任何地方使用;例如,在关卡加载时,我们可以调用UserData.getInstance().isSoundMuted()
,以确定是否应该对Music
对象调用play()
。
在第四步中,我们创建了将数据保存到设备的方法。这些方法非常直接,无论我们处理哪种数据,它们都应该相当相似。我们可以从参数中获取一个值,如setSoundMuted(pEnableSound)
,或者简单地递增,如unlockNextLevel()
中所示。
当我们最终想要将数据保存到设备上时,我们使用mEditor
对象,使用适合我们要存储的原始数据类型的方法,指定存储数据的键以及值。例如,对于关卡解锁,我们使用方法mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels)
,因为我们正在存储一个int
变量。对于boolean
变量,我们调用putBoolean(pKey, pValue)
,对于String
变量,我们调用putString(pKey, pValue)
,依此类推。
还有更多...
不幸的是,在客户端设备上存储数据时,无法保证用户不会访问数据以进行操纵。在 Android 平台上,大多数用户无法访问保存我们游戏数据的SharedPreferences
文件,但是拥有 root 权限的用户则能够查看该文件并根据需要做出修改。为了解释的方便,我们使用了明显的键名,比如soundKey
和unlockedLevels
。使用某种形式的混淆可以帮助让文件对于偶然在 root 设备上发现游戏数据的普通用户来说更像是一堆乱码。
如果我们想要进一步保护游戏数据,那么更为安全的做法是对偏好设置文件进行加密。Java 的javax.crypto.*
包是一个不错的起点,但请记住,加密和解密确实需要时间,这可能会增加游戏加载时间。
第二章:使用实体
在本章中,我们将开始探讨如何在屏幕上显示对象以及我们可以处理这些对象的多种方式。主题包括:
-
理解 AndEngine 实体
-
将原始图形应用到图层
-
使用精灵为场景注入生命
-
将文本应用到图层
-
使用相对旋转
-
重写
onManagedUpdate
方法 -
使用修饰符和实体修饰符
-
使用粒子系统
引言
在本章中,我们将开始使用 AndEngine 中包含的所有精彩的实体。实体为我们提供了一个基础,游戏世界中显示的每个对象都将依赖它,无论是分数文本、背景图像、玩家的角色、按钮以及所有其他内容。可以这样想,通过 AndEngine 的坐标系统,我们游戏中任何可以放置的对象在最基本的层面上都是一个实体。在本章中,我们将开始使用Entity
对象及其许多子类型,以便在我们的游戏中充分利用它们。
理解 AndEngine 实体
AndEngine 游戏引擎遵循实体-组件模型。实体-组件设计在当今许多游戏引擎中非常普遍,这有充分的理由。它易于使用,模块化,并且在所有游戏对象都可以追溯到单一的、最基本的Entity
对象的程度上非常有用。实体-组件模型可以被认为是游戏引擎对象系统最基本级别的“实体”部分。Entity
类只处理我们游戏对象依赖的最基本数据,比如位置、旋转、颜色、与场景的附加和分离等。而“组件”部分指的是Entity
类的模块化子类型,比如Scene
、Sprite
、Text
、ParticleSystem
、Rectangle
、Mesh
以及所有可以放入我们游戏中的其他对象。组件旨在处理更具体的任务,而实体则作为所有组件依赖的基础。
如何操作...
为了从最基础的Entity
方法开始,我们将一个Entity
对象附加到Scene
对象上:
创建并将一个Entity
对象附加到Scene
对象只需要以下两行代码:
Entity layer = new Entity();
mScene.attachChild(layer);
工作原理...
这里给出的两行代码允许我们创建一个基本的Entity
对象并将其附加到我们的Scene
对象上。正如本食谱中如何操作...一节所定义的,一个Entity
对象通常被用作图层。接下来几段将会讨论图层的用途。
实体在游戏开发中非常重要。在 AndEngine 中,事实是,我们场景上显示的所有对象都源自实体(包括Scene
对象本身!)。在大多数情况下,我们可以假设实体要么是场景上视觉显示的对象,如Sprite
、Text
或Rectangle
对象,要么是一个层,如Scene
对象。由于Entity
类的广泛性,我们将分别讨论实体的两种用途,好像它们是不同的对象。
实体的第一个,也可能是最重要的方面是分层能力。在游戏设计中,层是一个非常简单的概念;然而,由于游戏在游戏过程中倾向于支持大量的实体,在初次了解它们时,事情可能会很快变得混乱。我们必须将层视为一个具有一个父级和无限数量的子级的对象,除非另有定义。顾名思义,层的目的在于以有组织的方式将我们的各种实体对象应用到场景上,幸运的是,这也使我们能够对层执行一个会影响其所有子级一致的动作,例如,重新定位和施加某些实体修饰符。我们可以假设,如果我们有一个背景、一个中景和一个前景,那么我们的游戏将会有三个独立的层。这三个层将根据它们附加到场景的顺序以特定的顺序出现,就像将纸张堆叠在一起一样。如果我们从上往下看这个堆叠的纸张,最后添加到堆栈中的纸张将出现在其余纸张的前面。对于附加到Scene
对象的Entity
对象,同样的规则适用;这在前面的图片中显示:
前面的图片描绘了一个由三个Entity
对象层组成的基本游戏场景。这三个层都有特定的目的,即按照深度存储所有相关实体。首先应用到场景的是背景层,包括一个包含蓝天和太阳的精灵。接着应用到场景的是中景层。在这个层上,我们会找到与玩家相关的对象,包括玩家行走的景观、可收集的物品、敌人等等。最后,我们有了前景层,用于在设备的显示屏上显示最前面的实体。在所展示的图中,前景层用于显示用户界面,包括一个按钮和两个Text
对象。
让我们再次看看一个带有子实体附加层的场景可能是什么样子:
这张图显示了场景如何在屏幕上显示实体的深度/层次。在图的底部,我们有设备的显示。我们可以看到背景层首先附属于场景,然后是玩家层。这意味着附属于背景的实体将位于玩家层子实体的后面。记住这一点,这个规则同样适用于子实体。首先附着在层上的子实体在深度上将会位于任何随后附着物体的后面。
最后,关于一般 AndEngine 实体的一个最后一个关键主题是实体组合。在继续之前,我们应该了解的一个事实是子实体继承父实体的值!这是许多新的 AndEngine 开发者在设置游戏中的多层时遇到问题的地方。从倾斜、缩放、位置、旋转、可见性等所有属性,当父实体的属性发生变化时,子实体都会考虑在内。查看下面的图,它展示了 AndEngine 中实体的位置组合:
首先,我们应该了解在 AndEngine 的锚点中心分支中,坐标系统是从实体的左下角开始的。增加 x 值会将实体位置向右移动,增加 y 值会将实体位置向上移动。减少 x/y 值则会产生相反的效果。有了这个概念,我们可以看到附属于场景的较大矩形在场景上的位置被设定为坐标(6, 6)。由于较小矩形附属于较大矩形,而不是相对于场景的坐标系统,它实际上是使用大矩形的坐标系统。这意味着小矩形的锚点中心位置将直接位于大矩形坐标系统的(0, 0)位置上。正如我们在前一张图片中看到的,大矩形坐标系统上的(0, 0)位置是其左下角。
注意
旧的 AndEngine 分支与 AndEngine 最新的锚点中心分支之间的主要区别在于,定位实体不再意味着我们将实体的左上角设置在坐标系统上的一个位置。相反,实体的中心点将被放置在定义的位置上,这也在前面的图中有所展示。
还有更多...
AndEngine 中的 Entity
对象包含许多不同的方法,这些方法影响实体的许多方面。这些方法在塑造 Entity
对象的整体特性方面发挥着至关重要的作用,无论实体的子类型如何。为了完全控制实体的外观、反应、存储信息等,了解如何操作实体是一个好主意。使用以下列表来熟悉 Entity
对象的一些最重要的方法及其相应的获取方法。本章及后续章节将详细介绍此列表中未提及的方法。
-
setVisible(pBoolean)
和isVisible()
: 这个方法可以用来设置实体是否在场景中可见。将这些方法设置为true
将允许实体渲染,设置为false
将禁用渲染。 -
setChildrenVisible(pBoolean)
和isChildrenVisible()
: 类似于setVisible(pBoolean)
方法,不同之处在于它定义了调用实体的子实体的可见性,而不是自身。 -
setCullingEnabled(pBoolean)
和isCullingEnabled()
: 实体剔除可能是一种非常有前景的性能优化技术。更多详情请参见第八章中的通过实体剔除禁用渲染,最大化性能。 -
collidesWith(pOtherEntity)
: 这个方法用于检测调用此方法的实体与作为此方法参数提供的Entity
对象发生碰撞或重叠时。如果实体正在碰撞,此方法将返回true
。 -
setIgnoreUpdate(pBoolean)
和isIgnoreUpdate()
: 忽略实体更新可以提供明显的性能提升。更多详情请参见第八章中的忽略实体更新,最大化性能。 -
setChildrenIgnoreUpdate(pBoolean)
和isChildrenIgnoreUpdate()
: 类似于setIgnoreUpdate(pBoolean)
方法,不同之处在于它只影响调用实体的子实体,而不是自身。 -
getRootEntity()
: 这个方法将遍历实体的父实体,直到找到根父实体。找到根父实体后,此方法将返回根Entity
对象;在大多数情况下,根是我们的游戏Scene
对象。 -
setTag(pInt)
和getTag()
: 这个方法可以用来在实体中存储整数值。通常用于为实体设置标识值。 -
setParent(pEntity)
和hasParent()
: 将父实体设置为调用此方法的实体。hasParent()
方法根据调用实体是否有父实体返回true
或false
值。 -
setZIndex(pInt)
和getZIndex()
: 设置调用实体的Z
索引。值较大的实体将出现在值较小的实体前面。默认情况下,所有实体的Z
索引都是0
,这意味着它们将按照附加的顺序出现。更多详情请参见下面的sortChildren()
方法。 -
sortChildren()
: 在对实体或实体组的Z
索引进行修改后,必须在它们的父对象上调用此方法,修改后的效果才能在屏幕上显示。 -
setPosition(pX, pY)
或setPosition(pEntity)
: 此方法用于将实体的位置设置为特定的 x/y 值,或者可以用来设置到另一个实体的位置。此外,我们可以使用setX(pX)
和setY(pY)
方法仅对单个轴的位置进行更改。 -
getX()
和getY()
: 这些方法用于获取实体的本地坐标位置;即相对于其父对象的位置。 -
setWidth(pWidth)
和setHeight(pHeight)
或setSize(pWidth, pHeight)
: 这些方法用于设置调用实体的宽度和高度。此外,我们还可以使用getWidth()
和getHeight()
方法,它们将返回各自值作为浮点数据类型。 -
setAnchorCenter(pAnchorCenterX, pAnchorCenterY)
: 此方法用于设置实体的锚点中心。锚点中心是Entity
对象内部的一个位置,实体将围绕它旋转、倾斜和缩放。此外,修改锚点中心值将重新定位实体的“定位”锚点,从默认的中心点移动。例如,如果我们把锚点中心位置移动到实体的左上角,调用setPosition(0,0)
将会把实体的左上角放置在位置(0,0)
。 -
setColor(pRed, pGreen, pBlue)
和getColor()
: 此方法用于设置实体的颜色,颜色值从0.0f
(无颜色)到1.0f
(全颜色)不等。 -
setUserData(pObject)
和getUserData()
: 这两个方法在开发 AndEngine 游戏时非常有用。它们允许我们在实体中存储我们选择的任何对象,并在任何时刻修改或检索它。用户数据存储的一个可能用途是确定玩家角色持有什么类型的武器。充分利用这些方法吧!
将原始图形应用于图层
AndEngine 的原始类型包括 Line
、Rectangle
、Mesh
和 Gradient
对象。在本主题中,我们将重点关注 Mesh
类。Mesh 对象对于创建游戏中更为复杂的形状非常有用,其应用场景无限广阔。在本教程中,我们将使用 Mesh
对象来构建如下所示的房屋:
准备工作…
请参考代码包中名为 ApplyingPrimitives
的类。
如何操作…
为了创建一个Mesh
对象,我们需要比典型的Rectangle
或Line
对象做更多的工作。使用Mesh
对象有很多好处。它们可以帮助我们加强 OpenGL 坐标系统的技能,我们可以创建形状奇特的原始物体,并且能够改变单个顶点的位置,这对于某些类型的动画来说非常有用。
-
创建
Mesh
对象的第一步是创建我们的缓冲数据,这些数据用于指定构成网格形状的点:float baseBufferData[] = { /* First Triangle */ 0, BASE_HEIGHT, UNUSED, /* first point */ BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */ BASE_WIDTH, 0, UNUSED, /* third point */ /* Second Triangle */ BASE_WIDTH, 0, UNUSED, /* first point */ 0, 0, UNUSED, /* second point */ 0, BASE_HEIGHT, UNUSED, /* third point */ };
-
一旦缓冲数据配置完成,我们就可以继续创建
Mesh
对象。Mesh baseMesh = new Mesh((WIDTH * 0.5f) - (BASE_WIDTH * 0.5f), 0, baseBufferData, baseBufferData.length / POINTS_PER_TRIANGLE, DrawMode.TRIANGLES, mEngine.getVertexBufferObjectManager());
它是如何工作的…
让我们进一步分解这个过程,以了解我们是如何使用原始Mesh
对象制作房屋的。
在第一步中,我们创建baseMesh
对象的缓冲数据。这个缓冲数据用于存储 3D 空间中的点。缓冲数据中每三个值,由换行符分隔,构成 3D 世界中的一个顶点。但是,应该明白,由于我们使用的是 2D 游戏引擎,第三个值,即Z
索引,对我们来说是没有用的。因此,我们将每个顶点的第三个值定义为该食谱类中声明的UNUSED
常量,等于0
。每个三角形的点表示为(x, y, z)
,以避免混淆顺序。请参阅以下图表,了解第一步中定义的点如何绘制到网格上的矩形:
前一个图表展示了在如何操作…部分第一步中看到的baseMesh
对象的缓冲数据,或者说是绘制点。黑色线条代表第一组点:
0, BASE_HEIGHT, UNUSED, /* first point */
BASE_WIDTH, BASE_HEIGHT, UNUSED, /* second point */
BASE_WIDTH, 0, UNUSED, /* third point */
baseMesh
对象缓冲数据中的第二组点由灰色线条表示:
BASE_WIDTH, 0, UNUSED, /* first point */
0, 0, UNUSED, /* second point */
0, BASE_HEIGHT, UNUSED, /* third point */
由于BASE_HEIGHT
等于200
且BASE_WIDTH
等于400
,我们可以读取到第一个三角形的第一个点(0, BASE_HEIGHT)
位于矩形形状的左上角。顺时针移动,第一个三角形的第二个点位于(BASE_WIDTH, BASE_HEIGHT)
的位置,这将是矩形形状的右上角。显然,一个三角形由三个点组成,所以这让我们还有一个顶点要绘制。我们第一个三角形的最后一个顶点位于(BASE_WIDTH, 0)
的位置。作为一个个人挑战,使用前一个图中的场景图,找出灰色三角形的绘制点与缓冲数据相比如何!
在第二步中,我们将baseMesh
对象的缓冲区数据用来构建Mesh
对象。Mesh
对象是Entity
类的一个子类型,因此一旦我们创建了Mesh
对象,就可以对其进行重新定位、缩放、旋转以及进行其他必要的调整。按照构造函数中出现的顺序,参数如下:x 轴位置、y 轴位置、缓冲区数据、顶点数量、绘制模式和顶点缓冲对象管理器。前两个参数和最后一个参数对所有实体都是典型的,但缓冲区数据、顶点数量和绘制模式对我们来说是新的。缓冲区数据是数组,它指定了已绘制的顶点,这在第一步中已经介绍过。顶点数量只是缓冲区数据中包含的顶点数。我们缓冲数据中的每一个 x、y、z 坐标组成一个单独的顶点,这就是为什么我们用baseBufferData.length
值除以三来得到这个参数。最后,DrawMode
定义了Mesh
对象将如何解释缓冲区数据,这可以极大地改变网格的最终形状。不同的DrawMode
类型和用途可以在本主题的还有更多...部分中找到。
在继续之前,您可能会注意到“门”,或者更确切地说,代表门的蓝色线条并不是以与屋顶和基础Mesh
对象相同的方式创建的。相反,我们使用线条而不是三角形来绘制门的外框。请查看以下代码,它来自doorBufferData
数组,定义了线条连接的点:
0, DOOR_HEIGHT, UNUSED, /* first point */
DOOR_WIDTH, DOOR_HEIGHT, UNUSED, /* second point */
DOOR_WIDTH, 0, UNUSED, /* third point */
0, 0, UNUSED, /* fourth point */
0, DOOR_HEIGHT, UNUSED /* fifth point */
再次,如果我们绘制一个场景图,并像之前代表baseMesh
对象点的图那样标出这些点,我们实际上可以连接这些点,线条将形成一个矩形形状。一开始可能会让人感到困惑,尤其是在试图在脑海中创建形状时。从定义的顶点开始绘制自定义形状的诀窍是,在您喜欢的文档或图像编辑软件中保存一个空白场景图。创建一个类似于baseMesh
对象缓冲数据表示图的场景图,并使用它来标出点,然后简单地将点复制到代码中!
注意事项
需要特别记住的是,在之前场景图中的(0,0)
位置代表了Mesh
对象的中心。由于我们是向上和向右构建网格顶点,网格的锚定中心位置将不代表手动绘制的形状的中心!在构建Mesh
对象时,这一点非常重要。
还有更多...
对于初学者来说,创建网格可能是一个相当令人畏惧的主题,但有很多原因让我们习惯它们。AndEngine 开发者们的一个主要原因是它可以帮助我们理解 OpenGL 在较低层次上如何将形状绘制到显示上,这反过来又使我们更容易掌握更高层次的游戏开发功能。以下图片包含了 AndEngine 为我们提供的各种DrawMode
类型,以便以不同方式创建Mesh
对象:
前图展示了我们的缓冲数据中的顶点将如何根据所选的DrawMode
类型由Mesh
对象绘制到场景中。此图中的每个p#代表我们缓冲数据数组中的顶点(x,y 和 z 值)
。以下是每个DrawMode
类型的图像表示的解释:
-
DrawMode.POINTS
:这种选择允许我们在网格的缓冲数据中为每个顶点绘制单独的点。这些点不会由任何线条连接;它们仅仅在网格上为每个点显示一个点。 -
DrawMode.LINES
:这种选择允许我们在网格上绘制单独的线条。每两个顶点将由线条连接。 -
DrawMode.LINE_STRIP
:这种选择允许我们在网格上绘制点,第一个点之后的每个点都连接到前一个点。例如,p1将连接到p0,p2将连接到p1,依此类推。 -
DrawMode.LINE_LOOP
:这种选择与DrawMode.LINE_STRIP
类型类似,但是,第一个点与最后一个点也会由线条连接。这允许我们通过线条创建闭合的形状。 -
DrawMode.TRIANGLES
:这种选择允许我们在网格上为缓冲数据中定义的每组三个顶点绘制单独的三角形。这种绘制模式要求我们将顶点保持在三的倍数。 -
DrawMode.TRIANGLE_FAN
:这种选择允许我们绘制锥形或金字塔形状的网格。正如在之前的图中可以看到的,我们首先指定一个点,定义锥形的顶部点,然后继续指定形状的底部点。这种绘制模式需要定义三个或更多的顶点在缓冲数据中。 -
DrawMode.TRIANGLE_STRIP
:这种选择使我们能够轻松创建自定义的多边形网格。在初始化三角形的第三个顶点之后,缓冲数据中定义的每个顶点都会生成一个新的三角形,创建一个新的“带”。请参阅图表示例。这种绘制模式需要定义三个或更多的顶点在缓冲数据中。
另请参阅
- 本章节中提供的了解 AndEngine 实体。
使用精灵为场景带来生机
我们在这里讨论的可能是创建任何 2D 游戏最必要的一个方面。精灵(Sprites)允许我们在场景中显示 2D 图像,这些图像可以用来展示按钮、角色/化身、环境主题、背景以及游戏中可能需要通过图像文件来表示的任何其他实体。在本教程中,我们将介绍 AndEngine 的Sprite
实体的各个方面,这将为我们提供在以后更复杂的情况下继续使用Sprite
对象所需的信息。
准备工作...
在深入了解精灵如何创建的内部工作机制之前,我们需要了解如何创建和管理 AndEngine 的BitmapTextureAtlas
/BuildableBitmapTextureAtlas
对象以及ITextureRegion
对象。更多信息,请参考第一章,AndEngine 游戏结构中的教程,使用不同类型的纹理和应用纹理选项。
阅读完这些教程后,创建一个新的空 AndEngine 项目,使用BaseGameActivity
类,提供一个尺寸最大为 1024 x 1024 像素的 PNG 格式图像,将其命名为sprite.png
并放在项目的assets/gfx/
文件夹中,然后继续本教程的如何操作...部分。
如何操作...
我们只需几个快速步骤就可以创建并将精灵应用到我们的Scene
对象中。我们首先必须设置精灵将使用的必要纹理资源,创建Sprite
对象,然后必须将Sprite
对象附加到我们的Scene
对象。以下步骤将提供更多详细信息:
-
我们将从在
BaseGameActivity
类的onCreateResources()
方法中创建纹理资源开始。确保mBitmapTextureAtlas
和mSpriteTextureRegion
对象是全局变量,这样它们就可以在活动的各种生命周期方法中被访问:BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/"); /* Create the bitmap texture atlas for the sprite's texture region */ BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 256, 256, TextureOptions.BILINEAR); /* Create the sprite's texture region via the BitmapTextureAtlasTextureRegionFactory */ mSpriteTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "sprite.png"); /* Build the bitmap texture atlas */ try { mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1)); } catch (TextureAtlasBuilderException e) { e.printStackTrace(); } /* Load the bitmap texture atlas into the device's gpu memory */ mBitmapTextureAtlas.load();
-
接下来,我们将创建
Sprite
对象。我们可以在活动的onCreateScene()
或onPopulateScene()
方法中创建并附加Sprite
对象到Scene
对象。在它的构造函数中需要提供的参数包括,按此顺序,精灵的初始 x 坐标、初始 y 坐标、ITextureRegion
对象,最后是mEngine
对象的顶点缓冲区管理器:final float positionX = WIDTH * 0.5f; final float positionY = HEIGHT * 0.5f; /* Add our marble sprite to the bottom left side of the Scene initially */ Sprite mSprite = new Sprite(positionX, positionY, mSpriteTextureRegion, mEngine.getVertexBufferObjectManager()); The last step is to attach our Sprite to the Scene, as is necessary in order to display any type of Entity on the device's display: /* Attach the marble to the Scene */ mScene.attachChild(mSpriteTextureRegion);
它的工作原理...
如前一部分的步骤所示,实际上设置mBitmapTextureAtlas
和mSpriteTextureRegion
对象比专门创建和设置mSprite
对象需要更多的工作。因此,建议在开始之前先完成入门...部分提到的两个教程。
在第一步中,我们将创建适合我们sprite.png
图像需求的mBitmapTextureAtlas
和mSpriteTextureRegion
对象。在这一步中,请随意使用任何纹理选项或纹理格式。很好地了解它们是非常有想法的。
一旦我们创建了ITextureRegion
对象并且它已经准备好使用,我们可以进入第二步,创建Sprite
对象。创建一个精灵是一个直接的任务。前两个参数将用于定义精灵的初始位置,相对于其中心点。对于第三个参数,我们将传递在第一步中创建的ITextureRegion
对象,以便为场景中的精灵提供图像外观。最后,我们传递mEngine.getVertexBufferObjectManager()
方法,这是大多数实体子类型所必需的。
一旦我们的Sprite
对象被创建,我们必须在它能在设备上显示之前将它附加到Scene
对象,或者我们可以将它附加到已经连接到Scene
对象的另一个Entity
对象上。关于实体组合、放置以及其他各种必须了解的Entity
对象方面,请参阅本章中提供的了解 AndEngine 实体食谱。
还有更多内容...
没有某种形式的精灵动画,游戏是不完整的。毕竟,玩家只能在游戏中返回这么多次,之后就会对那些角色在屏幕上滑动而不动脚、攻击敌人时不挥舞武器,或者手榴弹只是消失而不是产生漂亮的爆炸效果的游戏感到厌倦。在这个时代,人们想要玩看起来和感觉都很棒的游戏,而像黄油般平滑的动画精灵没有什么能比得上“好极了!”,不是吗?
在第一章,AndEngine 游戏结构中的使用不同类型的纹理食谱中,我们介绍了如何创建一个TiledTextureRegion
对象,该对象允许我们将可用的精灵表作为纹理导入到游戏中。现在让我们找出如何使用TiledTextureRegion
对象与AnimatedSprite
对象为游戏的精灵添加动画。在这个演示中,代码将处理一个尺寸为 300 x 50 像素的图像。精灵表可以是如图所示的那样简单,以了解如何创建动画:
前图中的精灵表可用于创建一个有 12 列 1 行的TiledTextureRegion
对象。为这个精灵表创建BuildableBitmapTextureAtlas
和TiledTextureRegion
对象可以使用以下代码。但是,在导入这段代码之前,请确保在测试项目中全局声明纹理区域—TiledTextureRegion mTiledTextureRegion
。
/* Create the texture atlas at the same dimensions as the image (300x50)*/
BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 300, 50, TextureOptions.BILINEAR);
/* Create the TiledTextureRegion object, passing in the usual parameters,
* as well as the number of rows and columns in our sprite sheet for the
* final two parameters */
mTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(mBitmapTextureAtlas, this, "gfx/sprite_sheet.png", 12, 1);
/* Build and load the mBitmapTextureAtlas object */
try {
mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0));
} catch (TextureAtlasBuilderException e) {
e.printStackTrace();
}
mBitmapTextureAtlas.load();
既然我们的项目中已经有了可以操作的mTiledTextureRegion
精灵表,我们可以创建并动画化AnimatedSprite
对象。如果你使用的是如图所示带有黑色圆圈的精灵表,别忘了将Scene
对象的颜色改为非黑色,这样我们才能看到AnimatedSprite
对象:
/* Create a new animated sprite in the center of the scene */
AnimatedSprite animatedSprite = new AnimatedSprite(WIDTH * 0.5f, HEIGHT * 0.5f, mTiledTextureRegion, mEngine.getVertexBufferObjectManager());
/* Length to play each frame before moving to the next */
long frameDuration[] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200};
/* We can define the indices of the animation to play between */
int firstTileIndex = 0;
int lastTileIndex = mTiledTextureRegion.getTileCount();
/* Allow the animation to continuously loop? */
boolean loopAnimation = true;
* Animate the sprite with the data as set defined above */
animatedSprite.animate(frameDuration, firstTileIndex, lastTileIndex, loopAnimation, new IAnimationListener(){
@Override
public void onAnimationStarted(AnimatedSprite pAnimatedSprite,
int pInitialLoopCount) {
/* Fired when the animation first begins to run*/
}
@Override
public void onAnimationFrameChanged(AnimatedSprite pAnimatedSprite,
int pOldFrameIndex, int pNewFrameIndex) {
/* Fired every time a new frame is selected to display*/
}
@Override
public void onAnimationLoopFinished(AnimatedSprite pAnimatedSprite,
int pRemainingLoopCount, int pInitialLoopCount) {
/* Fired when an animation loop ends (from first to last frame) */
}
@Override
public void onAnimationFinished(AnimatedSprite pAnimatedSprite) {
/* Fired when an animation sequence ends */
}
);
mScene.attachChild(animatedSprite);
创建AnimatedSprite
对象可以按照本食谱中创建常规Sprite
对象的步骤进行。一旦创建完成,我们就可以设置其动画数据,包括单个帧的持续时间、要动画化的第一块和最后一块图块索引,以及是否要连续循环动画。注意,frameDuration
数组必须等于帧数!不遵循此规则将导致抛出IllegalArgumentException
异常。数据设置完成后,我们可以在AnimatedSprite
对象上调用animate()
方法,提供所有数据,并在需要时添加IAnimationListener
监听器。正如监听器中的注释所示,通过 AndEngine 的AnimatedSprite
类,我们对动画的控制能力得到了大幅提升。
使用 OpenGL 的抖动功能
在移动平台上开发视觉上吸引人的游戏时,我们很可能会希望图像中包含一些渐变,特别是在处理 2D 图形时。渐变非常适合创建光照效果、阴影以及许多其他无法应用于完整 2D 世界的对象。问题在于,我们是为移动设备开发,因此不幸的是,我们无法使用无限量的资源。因此,AndEngine 默认将表面视图的颜色格式下采样为RGB_565
。无论我们在纹理中定义的纹理格式如何,它们在设备上显示之前总是会被下采样。我们可以更改应用于 AndEngine 表面视图的颜色格式,但在开发包含许多精灵的大型游戏时,性能损失可能不值得。
这里,我们有两张具有渐变纹理的简单精灵的独立屏幕截图;这两种纹理都使用了RGBA_8888
纹理格式和BILINEAR
纹理过滤(最高质量)。
右侧的图像未经任何修改就应用到了Scene
对象上,而左侧的图像启用了 OpenGL 的抖动功能。这两张其他方面相同的图像之间的差异立即显而易见。抖动是我们对抗表面视图应用的降采样的一种很好的方法,而无需依赖最大颜色质量格式。简而言之,通过在图像颜色中加入低级别的随机噪声,结果得到了更平滑的完成效果,如左侧的图像所示。
在 AndEngine 中,为我们的实体应用抖动很简单,但与所有事物一样,明智的做法是选择哪些纹理应用抖动。该算法确实增加了一点额外的开销,如果使用过于频繁,可能会导致比简单地将我们的表面视图恢复为RGBA_8888
更大的性能损失。在以下代码中,我们在preDraw()
方法中启用抖动,在postDraw()
方法中禁用它:
@Override
protected void preDraw(GLState pGLState, Camera pCamera) {
// Enable dithering
pGLState.enableDither();
super.preDraw(pGLState, pCamera);
}
@Override
protected void postDraw(GLState pGLState, Camera pCamera) {
// Disable dithering
pGLState.disableDither();
super.postDraw(pGLState, pCamera);
}
晕染可以应用于 AndEngine 的Shape
类的任何子类型(Sprites
、Text
、基元等)。
注意
有关 OpenGL ES 2.0 以及如何使用所有不同函数的更多信息,请访问www.khronos.org/opengles/sdk/docs/man/
。
另请参阅
-
在第一章中处理不同类型的纹理,处理实体
-
在第一章中应用纹理选项,处理实体。
-
在本章中了解 AndEngine 实体。
将文本应用到图层
文本是游戏开发的重要组成部分,因为它可以用来动态显示积分系统、教程、描述等。AndEngine 还允许我们通过指定自定义的Font
对象来创建更适合个别游戏类型的文本样式。在本教程中,我们将创建一个Text
对象,它会随当前系统时间更新自身,并在字符串长度增长或缩短时调整其位置。这将为我们需要显示分数、时间和其他非特定动态字符串情况下的Text
对象使用做好准备。
准备就绪…
将Text
对象应用到我们的Scene
对象需要了解 AndEngine 的字体资源。请执行第一章中的教程,使用 AndEngine 字体资源,然后继续本教程的如何操作…部分。参考与此食谱活动代码捆绑中的名为ApplyingText
的类。
如何操作…
当我们将Text
对象应用到我们的Scene
对象上时,需要创建一个Font
对象来定义文本的样式,并创建Text
对象本身。以下步骤将说明我们必须采取的具体操作,以便在我们的场景上正确显示Text
对象:
-
创建任何
Text
对象的第一步是为自己准备一个Font
对象。Font
对象将作为定义Text
对象样式的资源。此外,我们还需要准备我们计划让Text
对象显示的字母:mFont = FontFactory.create(mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL), 32f, true, Color.WHITE); mFont.load(); /* * Prepare the mFont object for the most common characters used. This * will eliminate the need for the garbage collector to run when using a * letter/number that's never been used before */ mFont.prepareLetters("Time: 1234567890".toCharArray()); Once we've got our Font object created and ready for use, we can create the Text: /* Create the time Text object which will update itself as time passes */ Text mTimeText = new Text(0, timeTextHeight, mFont, TIME_STRING_PREFIX + TIME_FORMAT, MAX_CHARACTER_COUNT, mEngine.getVertexBufferObjectManager()) { // Overridden methods as seen in step 3... };
-
如果我们处理的是可能永远不会改变的最终字符串,那么只需要涵盖前两个步骤。然而,在本教程中,我们将需要覆盖
Text
实体的onManagedUpdate()
方法,以便随时间对其字符串进行调整。在本例中,每经过一秒钟,我们就会更新字符串的时间值:int lastSecond = 0; @Override protected void onManagedUpdate(float pSecondsElapsed) { Calendar c = Calendar.getInstance(); /* * We will only obtain the second for now in order to verify * that it's time to update the Text's string */ final int second = c.get(Calendar.SECOND); /* * If the last update's second value is not equal to the * current... */ if (lastSecond != second) { /* Obtain the new hour and minute time values */ final int hour = c.get(Calendar.HOUR); final int minute = c.get(Calendar.MINUTE); /* also, update the latest second value */ lastSecond = second; /* Build a new string with the current time */ final String timeTextSuffix = hour + ":" + minute + ":" + second; /* Set the Text object's string to that of the new time */ this.setText(TIME_STRING_PREFIX + timeTextSuffix); /* * Since the width of the Text will change with every change * in second, we should realign the Text position to the * edge of the screen minus half the Text's width */ this.setX(WIDTH - this.getWidth() * 0.5f); } super.onManagedUpdate(pSecondsElapsed); } Finally, we can make color adjustments to the Text and then attach it to the Scene or another Entity: /* Change the color of the Text to blue */ mTimeText.setColor(0, 0, 1); /* Attach the Text object to the Scene */ mScene.attachChild(mTimeText);
它是如何工作的…
在这一点上,我们应该已经了解了如何创建Font
对象,因为我们在第一章中已经讨论过。如果还不知道如何创建Font
对象,请访问第一章中的教程,使用 AndEngine 字体资源,处理实体。
在第一步中,我们只是创建了一个基本的Font
对象,它将为我们的Text
对象创建一个相当通用的样式。创建Font
对象后,我们只准备Text
对象在其生命周期内将显示的必要字符,使用mFont.prepareLetters()
方法。这样做可以避免在Font
对象内调用垃圾收集器。这个配方中使用的值显然是从0
到9
,因为我们处理的是时间,以及组成字符串Time:
的单个字符。
完成第一步后,我们可以进入第二步,创建Text
对象。Text
对象需要我们指定其在屏幕上的初始位置(x 和 y 坐标),使用的Font
对象样式,要显示的初始字符串,其最大字符数,以及所有Entity
对象所需的顶点缓冲对象管理器。然而,由于我们处理的这个Text
对象有一个动态更新的String
值,这将需要调整 x 轴,包括 x 坐标以及初始字符串在内的参数并不重要,因为它们将在更新Text
对象时频繁调整。最重要的参数是最大字符数。如果Text
对象的最大字符数超过了此参数内指定的值,将导致应用程序接收到ArrayIndexOutOfBoundsException
异常,很可能会需要终止。因此,我们在以下代码片段中累加最大字符串的长度:
private static final String TIME_STRING_PREFIX = "Time: ";
private static final String TIME_FORMAT = "00:00:00";
/* Obtain the maximum number of characters that our Text
* object will need to display*/
private static final int MAX_CHARACTER_COUNT = TIME_STRING_PREFIX.length() + TIME_FORMAT.length();
在第三步中,我们覆盖了Text
对象的onManagedUpdate()
方法,以便在每秒过去后对Text
对象的字符串应用更改。首先,我们只需获取设备的当前秒值,用它来与上一次调用Text
对象的onManagedUpdate()
方法中的秒值进行比较。这样,我们可以避免在每次更新时都使用系统时间更新Text
对象。如果Text
对象字符串上次更新的秒值与新的秒值不同,那么我们继续通过Calendar.getInstance().get(HOUR)
方法和MINUTE
变体获取当前的分钟和小时值。现在我们已经获得了所有的值,我们构建了一个包含更新时间的新字符串,并在Text
对象上调用setText(pString)
来更改它将在设备上显示的字符串。
然而,由于每个单独的字符宽度可能具有不同的值,我们也需要调整位置,以保持整个Text
对象在屏幕上。默认情况下,锚点位置被设置为Entity
对象的中心,因此通过调用this.setX(WIDTH - this.getWidth() * 0.5f)
(其中this
指的是Text
对象),我们将实体最中心的点定位在屏幕最大宽度右侧,然后减去实体宽度的一半。这将允许文本即使在其字符改变了Text
对象的宽度后,也能沿着屏幕边缘正确显示。
还有更多...
有时我们的游戏可能需要对Text
对象的字符串进行一些格式化处理。在我们需要调整Text
对象的水平对齐方式、如果字符串超出一定宽度则对文本应用自动换行,或者在文本前添加一个空格的情况下,我们可以使用一些非常易于使用的方法。以下方法可以直接在Text
对象上调用;例如,mText.setLeading(3)
:
-
setAutoWrap(pAutoWrap)
: 这个方法允许我们定义Text
实体是否执行自动换行,以及如何执行。我们可以为参数选择的选项包括AutoWrap.NONE
、AutoWrap.LETTERS
、AutoWrap.WORDS
和AutoWrap.CJK
。使用LETTERS
时,行中断不会在空白前等待,而WORDS
会等待。CJK
变体是允许对中、日、韩字符进行自动换行的选项。这个方法应该与setAutoWrapWidth(pWidth)
一起使用,其中pWidth
定义了Text
对象字符串中任意单行的最大宽度,在需要时导致换行。 -
setHorizontalAlign(pHorizontalAlign)
: 这个方法允许我们定义Text
对象字符串应遵循的对齐类型。参数包括HorizontalAlign.LEFT
、HorizontalAlign.CENTER
和HorizontalAlign.RIGHT
。其结果类似于我们在文本编辑器内设置对齐时看到的效果。 -
setLeading(pLeading)
: 这个方法允许我们在Text
对象字符串的开始处设置一个前置空间。所需的参数是一个浮点值,它定义了字符串的前导宽度。
另请参阅
-
在第一章中使用 AndEngine 字体资源,处理实体。
-
在本章中覆盖 onManagedUpdate 方法。
使用相对旋转
在 2D 空间中相对于其他实体的位置旋转实体是一个很棒的功能。相对旋转的使用是无限的,并且似乎总是移动游戏开发新手中的“热门话题”。这种技术被广泛应用的一个较为突出的例子是在塔防游戏中,它允许塔的炮塔朝向敌人(非玩家角色)行走的方向。在这个示例中,我们将介绍一种旋转我们的Entity
对象的方法,以便它们指向给定的 x/y 位置。以下图像展示了我们如何在场景上创建一个箭头,它会自动指向圆形图像的位置,无论它移动到哪里:
准备工作…
这个示例我们需要包含两个图像;一个名为marble.png
,尺寸为 32x32 像素,另一个名为arrow.png
,宽 31 像素,高 59 像素。弹珠可以是任何图像,我们只需随意在场景中拖动这个图像。箭头图像应该呈箭头形状,图像上的箭头直接朝上。请参考引言中的屏幕截图以了解需要包含的图像示例。将这些资源包含在空的BaseGameActivity
测试项目中,然后请参考代码包中的名为RelativeRotation
的类。
如何操作…
按照以下步骤操作:
-
在
BaseGameActivity
类中实现IOnSceneTouchListener
监听器:public class RelativeRotation extends BaseGameActivity implements IOnSceneTouchListener{
-
在
onCreateScene()
方法中设置Scene
对象的onSceneTouchListener
:mScene.setOnSceneTouchListener(this);
-
使用弹珠和小箭头的图像填充
Scene
对象。小箭头图像位于场景中心,而弹珠的位置会更新为任意触摸事件位置的坐标:/* Add our marble sprite to the bottom left side of the Scene initially */ mMarbleSprite = new Sprite(mMarbleTextureRegion.getWidth(), mMarbleTextureRegion.getHeight(), mMarbleTextureRegion, mEngine.getVertexBufferObjectManager()); /* Attach the marble to the Scene */ mScene.attachChild(mMarbleSprite); /* Create the arrow sprite and center it in the Scene */ mArrowSprite = new Sprite(WIDTH * 0.5f, HEIGHT * 0.5f, mArrowTextureRegion, mEngine.getVertexBufferObjectManager()); /* Attach the arrow to the Scene */ mScene.attachChild(mArrowSprite);
-
第四步介绍了
onSceneTouchEvent()
方法,它通过设备显示上的触摸事件处理弹珠图像的移动:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { // If a user moves their finger on the device if(pSceneTouchEvent.isActionMove()){ /* Set the marble's position to that of the touch even coordinates */ mMarbleSprite.setPosition(pSceneTouchEvent.getX(), pSceneTouchEvent.getY()); /* Calculate the difference between the two sprites x and y coordinates */ final float dX = mMarbleSprite.getX() - mArrowSprite.getX(); final float dY = mMarbleSprite.getY() - mArrowSprite.getY(); /* Calculate the angle of rotation in radians*/ final float angle = (float) Math.atan2(-dY, dX); /* Convert the angle from radians to degrees, adding the default image rotation */ final float rotation = MathUtils.radToDeg(angle) + DEFAULT_IMAGE_ROTATION; /* Set the arrow's new rotation */ mArrowSprite.setRotation(rotation); return true; } return false; }
工作原理…
在这个类中,我们创建了一个由箭头图像表示的精灵,并将其放置在屏幕正中心,自动指向由弹珠表示的另一个精灵。通过在BaseGameActivity
类中实现IOnSceneTouchListener
监听器,弹珠可以拖动。然后,我们将触摸监听器注册到mScene
对象。在实体根据另一个实体的位置进行旋转的情况下,我们不得不在某个持续更新的方法中包含旋转功能,否则我们的箭头不会持续反应。我们可以通过更新线程来实现这一点,但在这个实例中,我们将在重写的onSceneTouchEvent()
方法中包含该功能,因为直到我们触摸场景,“目标”实际上不会移动。
在第一步中,我们通过实现IOnSceneTouchListener
接口,允许我们的活动重写onSceneTouchEvent()
方法。一旦我们实现了触摸监听器,就可以进行第二步,让Scene
对象接收触摸事件,并根据活动重写的onSceneTouchEvent()
方法中的代码做出响应。这是通过setOnSceneTouchListener(pSceneTouchListener)
方法完成的。
在第四步中,if(pSceneTouchEvent.isActionMove())
条件语句判断是否有一个手指在场景上移动,更新大理石的位置,并在条件语句返回true
时计算箭头精灵的新旋转。
我们首先通过以下代码段中看到的setPosition(pX, pY)
方法,将大理石的位置更新到触摸的位置:
mMarbleSprite.setPosition(pSceneTouchEvent.getX(), pSceneTouchEvent.getY());
接下来,我们从目标的 x/y 坐标中减去指针的 x/y 坐标。这为我们提供了每个精灵坐标之间的差值,这将用于计算两个位置之间的角度。在这种情况下,指针是mArrowSprite
对象,目标是mMarbleSprite
对象:
/* Calculate the difference between the two sprites x and y coordinates */
final float dX = mMarbleSprite.getX() - mArrowSprite.getX();
final float dY = mMarbleSprite.getY() - mArrowSprite.getY();
/* Calculate the angle of rotation in radians*/
final float angle = (float) Math.atan2(-dY, dX);
最后,由于 AndEngine 的setRotation(pRotation)
方法使用度数,而atan2(pY, pX)
方法返回弧度,我们必须进行简单的转换。我们将使用 AndEngine 的MathUtils
类,其中包括一个radToDeg(pRadian)
方法,将我们的角度值从弧度转换为度数。一旦我们获得了正确的度数角度,我们将设置mArrowSprite
对象的旋转:
/* Convert the angle from radians to degrees, adding the default image rotation */
final float rotation = MathUtils.radToDeg(angle) + DEFAULT_IMAGE_ROTATION;
/* Set the arrow's new rotation */
mArrowSprite.setRotation(rotation);
需要注意的最后一点是,DEFAULT_IMAGE_ROTATION
值是一个表示90
度的int
值。这个值仅用于偏移mArrowSprite
精灵的旋转,否则我们将需要在我们的图像编辑软件中适当旋转图像。如果自定义图像中的指针没有指向图像的最顶部,这个值可能需要调整,以便将指针与目标对齐。
重写onManagedUpdate
方法
重写Entity
对象的onManagedUpdate()
方法在所有类型的情况下都非常有用。这样做,我们可以让我们的实体在每次通过更新线程更新实体时执行代码,每秒发生多次,除非实体被设置为忽略更新。可能性非常多,包括动画化我们的实体,检查碰撞,产生定时事件等等。使用我们的Entity
对象的onManagedUpdate()
方法还可以节省我们为单一实体创建和注册新的定时处理器以处理基于时间的事件。
准备就绪…
这个示例需要具备对 AndEngine 中Entity
对象的基本了解。请阅读本章提供的了解 AndEngine 实体的整个示例,然后创建一个新的空 AndEngine 项目,包含一个BaseGameActivity
类,并参考代码包中名为OverridingUpdates
的类。
如何操作…
在这个示例中,我们将创建两个Rectangle
对象。一个矩形将保持场景中心位置,持续旋转。第二个矩形将在场景中从左到右、从下到上连续移动,当到达右侧时重置回左侧,当到达场景顶部时重置回底部。此外,移动的矩形在与中心矩形碰撞时将变为绿色。所有这些移动和条件判断都将通过每个对象重写的onManagedUpdate(pSecondsElapsed)
方法来应用和执行。
-
重写第一个
Rectangle
对象的onManagedUpdate()
方法,以实现连续旋转:/* Value which defines the rotation speed of this Entity */ final int rotationIncrementalFactor = 25; /* Override the onManagedUpdate() method of this Entity */ @Override protected void onManagedUpdate(float pSecondsElapsed) { /* Calculate a rotation offset based on time passed */ final float rotationOffset = pSecondsElapsed * rotationIncrementalFactor; /* Apply the rotation offset to this Entity */ this.setRotation(this.getRotation() + rotationOffset); /* Proceed with the rest of this Entity's update process */ super.onManagedUpdate(pSecondsElapsed); }
-
重写第二个
Rectangle
对象的onManagedUpdate()
方法,以实现连续的位置更新、条件检查和碰撞检测:/* Value to increment this rectangle's position by on each update */ final int incrementXValue = 5; /* Obtain half the Entity's width and height values */ final float halfWidth = this.getWidth() * 0.5f; final float halfHeight = this.getHeight() * 0.5f; /* Override the onManagedUpdate() method of this Entity */ @Override protected void onManagedUpdate(float pSecondsElapsed) { /* Obtain the current x/y values */ final float currentX = this.getX(); final float currentY = this.getY(); /* obtain the max width and next height, used for condition checking */ final float maxWidth = currentX + halfWidth; final float nextHeight = currentY + halfHeight; // On every update... /* Increment the x position if this Entity is within the camera WIDTH */ if(maxWidth <= WIDTH){ /* Increase this Entity's x value by 5 pixels */ this.setX(currentX + incrementXValue); } else { /* Reset the Entity back to the bottom left of the Scene if it exceeds the mCamera's * HEIGHT value */ if(nextHeight >= HEIGHT){ this.setPosition(halfWidth, halfHeight); } else { /* if this Entity reaches the WIDTH value of our camera, move it * back to the left side of the Scene and slightly increment its y position */ this.setPosition(halfWidth, nextHeight); } } /* If the two rectangle's are colliding, set this rectangle's color to GREEN */ if(this.collidesWith(mRectangleOne) && this.getColor() != org.andengine.util.adt.color.Color.GREEN){ this.setColor(org.andengine.util.adt.color.Color.GREEN); /* If the rectangle's are no longer colliding, set this rectangle's color to RED */ } else if(this.getColor() != org.andengine.util.adt.color.Color.RED){ this.setColor(org.andengine.util.adt.color.Color.RED); } /* Proceed with the rest of this Entity's update process */ super.onManagedUpdate(pSecondsElapsed); }
工作原理…
在我们创建的第一个Rectangle
对象中,我们重写其onManagedUpdate(pSecondsElapsed)
方法,以持续更新旋转到新值。对于第二个Rectangle
对象,我们使其从屏幕最左侧连续移动到最右侧。当第二个矩形到达屏幕最右侧时,它会被重新定位到左侧,并将场景中的Rectangle
对象提高半个Rectangle
对象的高度。此外,当两个矩形重叠时,移动的矩形将改变颜色为绿色,直到它们不再接触。
第一步的代码允许我们在每次实体更新时创建一个事件。在这个特定的重写方法中,我们基于自上次更新以来经过的秒数计算Rectangle
对象的旋转偏移量。因为实体每秒更新多次,具体取决于设备能够达到的每秒帧数,我们将pSecondsElapsed
乘以25
以稍微增加旋转速度。否则,我们每次更新时将使实体沿0.01
度旋转,那样物体以该速率完成一次完整旋转将需要相当长的时间。我们可以在处理更新时利用pSecondsElapsed
更新,以便基于自上次更新以来经过的时间对事件进行修改。
第二步比第一步要复杂一些。在第二步中,我们覆盖了第二个矩形的onManagedUpdate()
方法,以便在每次实体更新时执行位置检查、碰撞检查以及更新矩形的定位。首先,我们声明了一些变量,这些变量将包含如实体当前位置、实体的半宽和半高值以便从锚点中心正确偏移,以及用于检查位置的下一个更新位置等值。这样做可以减少实体更新过程中所需计算的数量。如果在更新线程中应用了优化不佳的代码,很快就会导致帧率降低。尽可能多地使用方法调用和计算是很重要的;例如,在onManagedUpdate()
方法中多次获取currentX
值,比多次调用this.getX()
更为理想。
继续第二步中的位置检查和更新,我们首先确定矩形的锚点中心加上其半宽(由maxWidth
变量表示)是否小于或等于表示显示最右侧坐标的WIDTH
值。如果为真,我们会将矩形的 x 坐标增加incrementXValue
,即 5 个像素。另一方面,如果nextHeight
值大于或等于摄像机的HEIGHT
值,我们会将矩形对象重置回场景的左下角;或者如果矩形还没有到达显示顶部,只需将矩形的宽度增加其半宽并返回到左侧。
最后,我们在第二个Rectangle
对象的onManagedUpdate()
方法中拥有了碰撞检查方法。通过调用this.collidesWith(mRectangleOne)
,我们可以确定this
对象是否与指定对象(在本例中是mRectangleOne
)发生重叠。然后我们会进行一个额外的检查,以确定如果检测到碰撞,颜色是否已经等于我们打算将Rectangle
对象改变成的颜色;如果条件返回true
,则将Rectangle
对象设置为绿色。然而,如果每个更新都由多个Entity
对象执行,collidesWith()
可能是一个相当昂贵的碰撞检查方法!在这个示例中,我们纯粹是将此碰撞检查方法作为示例。一个可以考虑的选项是在执行碰撞检测之前,对两个对象进行轻量级的距离检测。
还有更多…
如前所述,所有子对象都会从其父对象接收到更新调用。在这种情况下,子实体也继承了父级修改后的pSecondsElapsed
值。我们甚至可以通过重写其onManagedUpdate()
方法并减少pSecondsElapsed
值,来减慢整个Scene
对象及其所有子对象的运行速度,如下所示:
super.onManagedUpdate(pSecondsElapsed * 0.5f);
将等于pSecondsElapsed
值一半的返回值传递给super
方法,将导致所有附加到该Scene
对象的实体在各个方面都减慢一半。这是在考虑游戏暂停或创建慢动作效果选项时需要记住的一点小技巧。
使用修改器和实体修改器
AndEngine 为我们提供了所谓的修改器和实体修改器。通过使用这些修改器,我们可以非常轻松地为实体应用整洁的效果。这些修改器在定义的时间范围内对Entity
对象应用特定的变化,如移动、缩放、旋转等。此外,我们还可以为实体修改器包含监听器和缓动函数,以完全控制它们的工作方式,这使得它们成为在我们的Scene
对象中应用某些类型动画的最强大方法之一。
注意
在继续之前,我们应该提到 AndEngine 中的修改器和实体修改器是两个不同的对象。修改器是直接应用于实体,随时间修改实体的属性,如缩放、移动和旋转。而实体修改器则用作任何数量的修改器的容器,处理一组修改器的执行顺序。这将在本食谱的后续内容中进一步讨论。
准备就绪…
此食谱需要了解 AndEngine 中Entity
对象的基础知识。请阅读本章提供的了解 AndEngine 实体的整个食谱,然后创建一个新的空 AndEngine 项目,包含一个BaseGameActivity
类,然后参考此食谱中的如何操作…部分。
如何操作…
在此食谱中,我们将介绍 AndEngine 的实体修改器,包括修改器监听器和缓动函数,以应用平滑的过渡效果。如果这听起来令人困惑,不必害怕!AndEngine 的修改器实际上非常易于使用,只需几个基本步骤就可以为我们的Entity
对象应用不同类型的动画。以下步骤涵盖了设置具有移动修改器的Entity
对象,这将引导我们进一步讨论实体修改器。将这些步骤中的代码导入到活动的onPopulateScene()
方法中:
-
创建并附加任何类型的实体到
Scene
对象。我们将为这个实体应用实体修改器:/* Define the rectangle's width/height values */ final int rectangleDimensions = 80; /* Define the initial rectangle position in the bottom * left corner of the Scene */ final int initialPosition = (int) (rectangleDimensions * 0.5f); /* Create the Entity which we will apply modifiers to */ Rectangle rectangle = new Rectangle(initialPosition, initialPosition, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager()); /* Set the rectangle's color to white so we can see it on the Scene */ rectangle.setColor(org.andengine.util.adt.color.Color.WHITE); /* Attach the rectangle to the Scene */ mScene.attachChild(rectangle);
-
一旦我们在
Scene
对象上放置了一个实体,我们就可以开始创建我们的修改器了。在这一步中,我们将创建一个MoveModifier
对象,它允许我们随时间对实体的位置进行更改。但首先,我们将定义其值:/* Define the movement modifier values */ final float duration = 3; final float fromX = initialPosition; final float toX = WIDTH - rectangleDimension * 0.5f; final float fromY = initialPosition; final float toY = HEIGHT - rectangleDimension * 0.5f; /* Create the MoveModifier with the defined values */ MoveModifier moveModifier = new MoveModifier(duration, fromX, fromY, toX, toY);
-
现在我们已经创建并设置好了
moveModifier
对象,我们可以通过以下调用将此修改器注册到我们希望的任何实体上,这将开始移动效果:/* Register the moveModifier to our rectangle entity */ rectangle.registerEntityModifier(moveModifier);
它的工作原理是……
实体修改器的话题相当广泛,因此我们将从步骤开始深入。从那里,我们将使用这些步骤作为基础,以便进一步深入到关于实体修改器使用更复杂的讨论和示例。
在第一步中,我们只是创建了一个Entity
对象,在这个案例中是一个Rectangle
,我们将用它作为应用修改器的测试对象。只需将此步骤中的代码添加到onPopulateScene()
方法中;在接下来的修改器和实体修改器“实验”中,这段代码将保持不变。
在第二步中,我们将开始使用最基本的修改器之一,当然是MoveModifier
。这个修改器允许我们定义移动的起始位置、结束位置以及从起点到终点移动所需的秒数。正如我们所看到的,这非常简单,修改器最值得注意的是,在大多数情况下,这就是设置大多数修改器所需的全部内容。所有修改器真正需要的是一个“from”值、一个“to”值以及定义“from-to”发生秒数的时长。记住这一点,在大多数情况下,使用修改器将会非常轻松!
接下来,在第三步中,我们只需通过registerEntityModifier(pModifier)
方法将我们新创建的moveModifier
对象应用到rectangle
对象上。这将使moveModifier
效果应用到矩形上,首先将其定位到“from”坐标,然后在 3 秒的时间内移动到“to”坐标。
我们知道,要向Entity
对象注册修改器或实体修改器,可以调用entity.registerEntityModifier(pEntityModifier)
,但我们也应该知道,一旦完成修改器,我们应该将其从Entity
对象中移除。我们可以通过调用entity.unregisterEntityModifier(pEntityModifier)
来实现,或者如果我们想移除附加到Entity
对象的所有实体修改器,可以调用entity.clearEntityModifiers()
。另一方面,如果一个修改器或实体修改器运行了完整的时长,而我们还没有准备好从实体中移除它,我们必须调用modifier.reset()
以重新播放效果。或者,如果我们想在重新播放效果之前对修改器进行微调,可以调用modifier.reset(duration, fromValue, toValue)
。其中reset
方法中的参数将相对于我们要重置的修改器类型。
moveModifier
对象有效,但它非常无聊!毕竟,我们只是在将一个矩形从场景的左下角移动到右上角。幸运的是,这只是修改器应用表面的刮擦。以下小节包含了 AndEngine 能够应用到我们的Entity
对象的所有修改器的参考,必要时还提供了示例。
AndEngine 的修改器
以下是我们可以应用到实体上的所有 AndEngine 修改器的集合。更高级的修改器将提供一个快速示例代码片段。在介绍它们时,请随意在您的测试项目中尝试:
-
AlphaModifier
:使用这个修改器,可以随时间调整实体的透明度值。构造函数的参数包括持续时间、起始透明度和结束透明度,依次排列。 -
ColorModifier
:使用这个修改器,可以随时间调整实体的颜色值。构造函数的参数包括持续时间、起始红色、结束红色、起始绿色、结束绿色、起始蓝色和结束蓝色,依次排列。 -
DelayModifier
:这个修改器旨在分配给实体修改器对象,以便在一个修改器被执行和另一个修改器被执行之间提供延迟。参数包括持续时间。 -
FadeInModifier
:基于AlphaModifier
类,FadeInModifier
修改器在定义的持续时间内在构造函数中提供,将实体的透明度值从0.0f
更改为1.0f
。 -
FadeOutModifier
:与FadeOutModifier
类似,只不过透明度值被交换了。 -
JumpModifier
:这个修改器可以用来向实体应用“跳跃”动作。参数包括持续时间、起始 X、结束 X、起始 Y、结束 Y 和跳跃高度。这些值将定义在定义的持续时间内在视觉上实体跳跃的距离和高度。 -
MoveByModifier
:这个修改器允许我们偏移实体的位置。参数包括持续时间、X 偏移和 Y 偏移,依次排列。例如,指定一个偏移量为-15
将使实体在场景上向左移动 15 个单位。 -
MoveXModifier
和MoveYModifier
:这些修改器与MoveModifier
类似,允许我们向实体提供移动。然而,这些方法只根据方法名称确定在单个轴上应用移动。参数包括持续时间、起始坐标和结束坐标,依次排列。 -
RotationAtModifier
:这个修改器允许我们在偏移旋转中心的同时向实体应用旋转。参数包括持续时间、起始旋转、结束旋转、旋转中心 X 和旋转中心 Y。 -
RotationByModifier
:这个修改器允许我们偏移实体的当前旋转值。参数包括持续时间和旋转偏移值。例如,提供一个旋转偏移值为90
将使实体顺时针旋转九十度。 -
RotationModifier
:这个修改器允许我们从一个特定值旋转实体到另一个特定值。参数包括持续时间、起始旋转和目标旋转。 -
ScaleAtModifier
:这个修改器允许我们在缩放时偏移缩放中心来缩放实体。参数包括持续时间、起始缩放、目标缩放、缩放中心 x 和缩放中心 y。 -
ScaleModifier
:这个修改器允许我们从一个特定值缩放实体到另一个特定值。参数包括持续时间、起始缩放和目标缩放,按此顺序。 -
SkewModifier
:这个修改器允许我们随时间改变实体的 x 和 y 值。参数包括持续时间、起始斜切 x、目标斜切 x、起始斜切 y 和目标斜切 y,顺序是特定的。 -
PathModifier
:这个修改器相对于MoveModifier
,不过我们可以添加任意多的“到”坐标。这使得我们可以在Scene
对象上为实体创建一个路径,通过为PathModifier
修改器指定 x/y 坐标对来跟随。在以下步骤中,我们将了解如何为我们的实体创建一个PathModifier
修改器:-
定义路径的航点。x 和 y 坐标的航点数组应该具有相同数量的点,因为它们将按顺序配对以形成
PathModifier
的单个 x/y 坐标。我们必须在每个数组中至少设置两个点,因为我们需要至少一个起始点和结束点:/* Create a list which specifies X coordinates to follow */ final float pointsListX[] = { initialPosition, /* First x position */ WIDTH - initialPosition, /* Second x position */ WIDTH - initialPosition, /* Third x position */ initialPosition, /* Fourth x position */ initialPosition /* Fifth x position */ }; /* Create a list which specifies Y coordinates to follow */ final float pointsListY[] = { initialPosition, /* First y position */ HEIGHT - initialPosition, /* Second y position */ initialPosition, /* Third y position */ HEIGHT - initialPosition, /* Fourth y position */ initialPosition /* Fifth y position */ };
-
创建一个
Path
对象,我们将使用它将分开数组中的各个点配对成航点。我们通过遍历数组并在path
对象上调用to(pX, pY)
方法来实现这一点。请注意,每次我们调用这个方法,我们都在path
对象中添加一个额外的航点:/* Obtain the number of control points we have */ final int controlPointCount = pointsListX.length; /* Create our Path object which we will pair our x/y coordinates into */ org.andengine.entity.modifier.PathModifier.Path path = new Path(controlPointCount); /* Iterate through our point lists */ for(int i = 0; i < controlPointCount; i++){ /* Obtain the coordinates of the control point at the index */ final float positionX = pointsListX[i]; final float positionY = pointsListY[i]; /* Setup a new way-point by pairing together an x and y coordinate */ path.to(positionX, positionY); }
-
最后,一旦我们定义了航点,就可以创建
PathModifier
对象,提供持续时间以及我们的path
对象作为参数:/* Movement duration */ final float duration = 3; /* Create the PathModifier */ PathModifier pathModifier = new PathModifier(duration, path); /* Register the pathModifier object to the rectangle */ rectangle.registerEntityModifier(pathModifier);
-
-
CardinalSplineMoveModifier
:这是我们最后要讨论的修改器。这个修改器与PathModifier
修改器相对相似,不过我们可以对Entity
对象的移动施加张力。这允许在接近拐角或改变方向时实现更流畅和平滑的移动,实际上看起来相当不错。在以下步骤中,我们将了解如何为我们的实体创建一个CardinalSplineMoveModifier
修改器:-
第一步与
PathModifier
修改器类似,是创建我们的点数组。在这个例子中,我们可以从PathModifier
示例的第一步复制代码。然而,这个修改器与PathModifier
对象的一个区别在于,我们需要至少 4 个单独的 x 和 y 点。 -
第二步是确定控制点的数量,定义张力,并创建一个
CardinalSplineMoveModifierConfig
对象。这是CardinalSplineMoveModifier
修改器的PathModifier
修改器中Path
对象的等价物。张力可以在-1
到1
之间,不能多也不能少。张力为-1
将使Entity
对象的移动非常松散,在转角和方向变化时非常松散;而张力为1
将非常像PathModifier
修改器,在移动上非常严格:/* Obtain the number of control points we have */ final int controlPointCount = pointsListX.length; /* Define the movement tension. Must be between -1 and 1 */ final float tension = 0f; /* Create the cardinal spline movement modifier configuration */ CardinalSplineMoveModifierConfig config = new CardinalSplineMoveModifierConfig(controlPointCount, tension);
-
在第三步中,与
PathModifier
修改器非常相似,我们必须将 x/y 坐标配对在我们的点数组中,不过在这个情况下,我们是将它们存储在config
对象中:/* Iterate through our control point indices */ for(int index = 0; index < controlPointCount; index++){ /* Obtain the coordinates of the control point at the index */ final float positionX = pointsListX[index]; final float positionY = pointsListY[index]; /* Set position coordinates at the current index in the config object */ config.setControlPoint(index, positionX, positionY); }
-
接下来,我们只需简单地定义移动的持续时间,创建
CardinalSplineMoveModifier
修改器,提供持续时间和config
对象作为参数,并最终将修改器注册到Entity
对象上:/* Movement duration */ final float duration = 3; /* Create the cardinal spline move modifier object */ CardinalSplineMoveModifier cardinalSplineMoveModifier = new CardinalSplineMoveModifier(duration, config); /* Register the cardinalSplineMoveModifier object to the rectangle object */ rectangle.registerEntityModifier(cardinalSplineMoveModifier);
-
现在我们已经对可以应用到实体上的各个修改器有了深入的理解,我们将介绍 AndEngine 中的三个主要实体修改器以及它们的用途。
AndEngine 的实体修改器
AndEngine 包含三种实体修改器对象,用于通过将两个或更多修改器组合成一个单一事件或序列,为我们的Entity
对象构建复杂的动画。这三种不同的实体修改器包括LoopEntityModifier
、ParallelEntityModifier
和SequenceEntityModifier
对象。接下来,我们将描述这些实体修改器的具体细节和示例,展示如何将它们组合成单一动画事件。
-
LoopEntityModifier
:这个实体修改器允许我们无限次数或指定次数(如果提供了第二个int
参数)循环指定的修改器。这是最简单的实体修改器。一旦我们设置好了LoopEntityModifier
,就可以直接将其应用于Entity
对象:/* Define the move modifiers properties */ final float duration = 3; final float fromX = 0; final float toX = 100; /* Create the move modifier */ MoveXModifier moveXModifier = new MoveXModifier(duration, fromX, toX); /* Create a loop entity modifier, which will loop the move modifier * indefinitely, or until unregistered from the rectangle. * If we want to provide a loop count, we can add a second int parameter * to this constructor */ LoopEntityModifier loopEntityModifier = new LoopEntityModifier(moveXModifier); /* register the loopEntityModifier to the rectangle */ rectangle.registerEntityModifier(loopEntityModifier);
-
ParallelEntityModifier
:这个实体修改器允许我们将无限数量的修改器组合成一个单一动画。这个实体修改器提供的参数中的修改器将同时运行在Entity
对象上。这使得我们可以在旋转时缩放修改器,例如,在以下示例中可以看到。欢迎在示例中添加更多修改器进行练习:/* Scale modifier properties */ final float scaleDuration = 2; final float fromScale = 1; final float toScale = 2; /* Create a scale modifier */ ScaleModifier scaleModifier = new ScaleModifier(scaleDuration, fromScale, toScale); /* Rotation modifier properties */ final float rotateDuration = 3; final float fromRotation = 0; final float toRotation = 360 * 4; /* Create a rotation modifier */ RotationModifier rotationModifier = new RotationModifier(rotateDuration, fromRotation, toRotation); /* Create a parallel entity modifier */ ParallelEntityModifier parallelEntityModifier = new ParallelEntityModifier(scaleModifier, rotationModifier); /* Register the parallelEntityModifier to the rectangle */ rectangle.registerEntityModifier(parallelEntityModifier);
-
SequenceEntityModifier
:这个实体修改器允许我们将修改器串联起来,在单个Entity
对象上按顺序执行。这个修改器是在之前提到的修改器列表中使用DelayModifier
对象的理想实体修改器。以下示例显示了一个从屏幕左下角移动到屏幕中心的Entity
对象,暂停2
秒,然后缩小到比例因子为0
:/* Move modifier properties */ final float moveDuration = 2; final float fromX = initialPosition; final float toX = WIDTH * 0.5f; final float fromY = initialPosition; final float toY = HEIGHT * 0.5f; /* Create a move modifier */ MoveModifier moveModifier = new MoveModifier(moveDuration, fromX, fromY, toX, toY); /* Create a delay modifier */ DelayModifier delayModifier = new DelayModifier(2); /* Scale modifier properties */ final float scaleDuration = 2; final float fromScale = 1; final float toScale = 0; /* Create a scale modifier */ ScaleModifier scaleModifier = new ScaleModifier(scaleDuration, fromScale, toScale); /* Create a sequence entity modifier */ SequenceEntityModifier sequenceEntityModifier = new SequenceEntityModifier(moveModifier, delayModifier, scaleModifier); /* Register the sequenceEntityModifier to the rectangle */ rectangle.registerEntityModifier(sequenceEntityModifier);
更重要的是要知道我们可以将SequenceEntityModifier
修改器添加到ParallelEntityModifier
修改器中,将ParallelEntityModifier
修改器添加到LoopEntityModifier
修改器中,或者是我们能想到的任何其他组合!这使得修改器和实体修改器的可能性变得极其广泛,并允许我们以相当大的便利性为实体创建极其复杂的动画。
还有更多内容...
在继续下一个主题之前,我们应该看看为实体修改器包含的额外特性。还有两个参数我们可以传递给实体修改器,我们之前还没有讨论过;那就是修改器监听器和缓动函数。这两个类可以帮助我们使修改器比我们在如何工作...部分看到的更加定制化。
IEntityModifierListener
监听器可以用来在修改器开始和结束时触发事件。在以下代码段中,我们只是简单地向 logcat 打印日志,以通知我们修改器何时开始和结束。
IEntityModifierListener entityModifierListener = new IEntityModifierListener(){
// When the modifier starts, this method is called
@Override
public void onModifierStarted(IModifier<IEntity> pModifier,
IEntity pItem) {
Log.i("MODIFIER", "Modifier started!");
}
// When the modifier finishes, this method is called
@Override
public void onModifierFinished(final IModifier<IEntity> pModifier,
final IEntity pItem) {
Log.i("MODIFIER", "Modifier started!");
}
};
modifier.addModifierListener();
之前的代码展示了一个带有基本日志输出的修改器监听器的框架。在更接近游戏开发的场景中,一旦修改器完成,我们可以调用pItem.setVisible(false)
。例如,这可以用于处理场景中细微的落叶或雨滴,这些落叶或雨滴离开了摄像头的视野。然而,我们决定用监听器来做什么完全取决于我们自己的判断。
最后,我们将快速讨论 AndEngine 中的缓动函数。缓动函数是给实体修改器添加额外“酷炫”层次的好方法。习惯了修改器之后,缓动函数可能会真正吸引你,因为它们给修改器带来了所需的额外动力,以产生完美效果。解释缓动函数的最好方法是想象一个游戏,菜单按钮从屏幕顶部落下并“弹跳”到位。这里的弹跳就是我们的缓动函数产生效果的情况。
/* Move modifier properties */
final float duration = 3;
final float fromX = initialPosition;
final float toX = WIDTH - initialPosition;
final float fromY = initialPosition;
final float toY = HEIGHT - initialPosition;
/* Create a move modifier with an ease function */
MoveModifier moveModifier = new MoveModifier(duration, fromX, fromY, toX, toY, org.andengine.util.modifier.ease.EaseElasticIn.getInstance());
rectangle.registerEntityModifier(moveModifier);
正如我们在这里看到的,给修改器应用缓动函数只需在修改器的构造函数中添加一个额外的参数即可。通常最困难的部分是选择使用哪一个,因为缓动函数列表相当长。花些时间查看org.andengine.util.modifier.ease
包提供的各种缓动函数。只需将前一段代码中的EaseElasticIn
替换为你想要测试的缓动函数,然后重新构建项目以查看效果!
提示
缓动函数参考
从 Google Play 将AndEngine – Examples应用程序下载到你的设备上。打开应用程序并找到Using EaseFunctions的例子。尽管与最新的 AndEngine 分支相比,示例应用程序相当过时,但缓动函数示例仍然是一个绝对有效的工具,用于确定哪些缓动函数最适合我们游戏的需求!
另请参阅
- 本章节中了解 AndEngine 实体。
使用粒子系统
粒子系统可以为我们的游戏提供非常吸引人的效果,涵盖游戏中的许多不同事件,如爆炸、火花、血腥、雨等。在本章中,我们将介绍 AndEngine 的ParticleSystem
类,这将用于创建定制化的粒子效果,满足我们的各种需求。
准备工作…
本食谱需要了解 AndEngine 中Sprite
对象的基础知识。请阅读第一章中的整个食谱,使用不同类型的纹理以及本章中给出的了解 AndEngine 实体。接下来,创建一个带有BaseGameActivity
类的新的空 AndEngine 项目,并从代码包中的WorkingWithParticles
类导入代码。
如何操作…
为了开始在 AndEngine 中创建粒子效果,我们需要至少三个对象。这些对象包括代表生成的单个粒子的ITextureRegion
对象,一个ParticleSystem
对象和一个ParticleEmitter
对象。一旦我们有了这些,我们就可以开始向我们的粒子系统添加所谓的粒子初始化器和粒子修改器,以创建我们自己的个性化效果。以下步骤将指导如何设置一个基本的粒子系统,以便在此基础上进行构建。
-
第一步涉及决定我们希望粒子系统生成的图像。这可以是任何图像、任何颜色和任何大小。随意创建一个图像,并设置
BuildableBitmapTextureAtlas
和ITextureRegion
来将图像加载到测试项目的资源中。为了保持事情简单,请将图像的尺寸控制在 33x33 像素以下以适应本食谱。 -
创建
ParticleEmitter
对象。现在我们将使用PointParticleEmitter
对象子类型:/* Define the center point of the particle system spawn location */ final int particleSpawnCenterX = (int) (WIDTH * 0.5f); final int particleSpawnCenterY = (int) (HEIGHT * 0.5f); /* Create the particle emitter */ PointParticleEmitter particleEmitter = new PointParticleEmitter(particleSpawnCenterX, particleSpawnCenterY);
-
创建
ParticleSystem
对象。我们将使用BatchedSpriteParticleSystem
对象实现,因为它是 AndEngine 中包含的最新和最好的ParticleSystem
对象子类型。它允许我们创建大量粒子,同时大大降低典型SpriteParticleSystem
对象的开销:/* Define the particle system properties */ final float minSpawnRate = 25; final float maxSpawnRate = 50; final int maxParticleCount = 150; /* Create the particle system */ BatchedSpriteParticleSystem particleSystem = new BatchedSpriteParticleSystem( particleEmitter, minSpawnRate, maxSpawnRate, maxParticleCount, mTextureRegion, mEngine.getVertexBufferObjectManager());
-
在创建粒子系统的最后一步中,我们将添加任意组合的粒子发射器和粒子修改器,然后将粒子系统附加到
Scene
对象上:/* Add an acceleration initializer to the particle system */ particleSystem.addParticleInitializer(new AccelerationParticleInitializer<UncoloredSprite>(25f, -25f, 50f, 100f)); /* Add an expire initializer to the particle system */ particleSystem.addParticleInitializer(new ExpireParticleInitializer<UncoloredSprite>(4)); /* Add a particle modifier to the particle system */ particleSystem.addParticleModifier(new ScaleParticleModifier<UncoloredSprite>(0f, 3f, 0.2f, 1f)); /* Attach the particle system to the Scene */ mScene.attachChild(particleSystem);
它是如何工作的…
对于许多新的 AndEngine 开发者来说,处理粒子似乎是一个相当困难的课题,但实际上恰恰相反。在 AndEngine 中创建粒子效果非常简单,但如往常一样,我们应该学会走再尝试飞!在本食谱的步骤中,我们设置了一个相当基础的粒子系统。随着话题的深入,我们将讨论并插入粒子系统的其他模块化组件,以拓宽我们对构成复杂粒子系统效果各个部分的知识。
在第一步中,我们需要建立一个ITextureRegion
对象来为我们的粒子系统提供资源。ITextureRegion
对象将视觉上代表每个生成的独立粒子。纹理区域可以是任何大小,但通常它们会在 2 x 2 到 32 x 32 像素之间。请记住,粒子系统旨在生成大量的对象,因此ITextureRegion
对象越小,就粒子系统而言性能会越好。
在第二步中,我们创建了一个粒子发射器并将其置于Scene
对象的中心。粒子发射器是粒子系统中的一个组件,它控制着粒子的初始生成位置。在本食谱中,我们使用的是PointParticleEmitter
对象类型,它会简单地在场景上以particleSpawnCenterX
和particleSpawnCenterY
变量定义的相同坐标生成所有粒子。AndEngine 还包括其他四种粒子发射器类型,我们稍后会进行讨论。
当我们创建并适当地设置好粒子发射器后,我们可以进入第三步并创建BatchedSpriteParticleSystem
对象。我们需要按顺序向BatchedSpriteParticleSystem
对象传递的参数包括:粒子发射器、粒子的最小生成速率、最大生成速率、可以同时显示的最大粒子数量、粒子应视觉代表的ITextureRegion
对象,以及mEngine
对象的顶点缓冲区对象管理器。
最后,在第四步中,我们添加了一个AccelerationParticleInitializer
对象,它将为粒子提供加速运动,使它们不仅仅停留在它们产生的地方。我们还添加了一个ExpireParticleInitializer
对象,用于在定义的时间后销毁粒子。如果没有某种初始化器或修改器移除粒子,BatchedParticleSystem
对象最终会达到其最大粒子限制,并停止产生粒子。最后,我们向粒子系统添加了一个ScaleParticleModifier
对象,它将随时间改变每个粒子的缩放比例。这些粒子初始化器和粒子修改器将稍作深入解释,现在只需知道这是我们应用它们到粒子系统的步骤。添加完我们选择的初始化器和修改器后,我们将particleSystem
对象附加到Scene
对象上。
完成这四个步骤后,粒子系统将开始产生粒子。然而,我们可能并不总是希望粒子从特定的粒子系统中产生。要禁用粒子产生,可以调用particleSystem.setParticlesSpawnEnabled(false)
,或者设置为true
以重新启用粒子产生。除了这个方法,BatchedSpriteParticleSystem
对象还包含Entity
对象的所有普通功能和方法。
有关粒子系统的各个组成部分的更多信息,请参见以下子主题。这些主题包括粒子发射器、粒子初始化器和粒子修改器。
粒子发射器的选择
AndEngine 包含五种可立即使用的粒子发射器,它们可以改变场景上粒子的初始放置,这不应与定义粒子发射器位置混淆。有关每个粒子发射器的工作原理,请查看粒子发射器列表。请随时在步骤二的配方中用以下列表中的粒子发射器替换粒子发射器。
-
PointParticleEmitter
:这是最基础的粒子发射器;这种粒子发射器使所有产生的粒子在场景上同一定义的位置产生。粒子产生的位置不会有任何变化。然而,可以通过调用pointParticleEmitter.setCenter(pX, pY)
方法来改变粒子发射器的位置,其中pX
和pY
定义了产生粒子的新坐标。 -
CircleOutlineParticleEmitter
:这种粒子发射器子类型将使粒子在圆形轮廓的位置产生。这个发射器构造函数中需要包含的参数包括 x 坐标、y 坐标和一个定义圆形轮廓整体大小的半径。请看以下示例:/* Define the center point of the particle system spawn location */ final int particleSpawnCenterX = (int) (WIDTH * 0.5f); final int particleSpawnCenterY = (int) (HEIGHT * 0.5f); /* Define the radius of the circle for the particle emitter */ final float particleEmitterRadius = 50; /* Create the particle emitter */ CircleOutlineParticleEmitter particleEmitter = new CircleOutlineParticleEmitter(particleSpawnCenterX, particleSpawnCenterY, particleEmitterRadius);
-
CircleParticleEmitter
:这种粒子发射器子类型允许粒子在CircleOutlineParticleEmitter
对象仅限于边缘轮廓的圆形区域内任何位置生成。CircleParticleEmitter
对象在其构造函数中需要与CircleOutlineParticleEmitter
对象相同的参数。要测试这种粒子发射器子类型,只需将CircleOutlineParticleEmitter
示例中的对象重构为使用CircleParticleEmitter
对象即可。 -
RectangleOutlineParticleEmitter
:这种粒子发射器子类型将导致粒子从由构造函数参数定义大小的矩形的四个角生成。与CircleOutlineParticleEmitter
对象不同,这种粒子发射器不允许粒子围绕矩形的整个边缘生成。请参阅以下示例:/* Define the center point of the particle system spawn location */ final int particleSpawnCenterX = (int) (WIDTH * 0.5f); final int particleSpawnCenterY = (int) (HEIGHT * 0.5f); /* Define the width and height of the rectangle particle emitter */ final float particleEmitterWidth = 50; final float particleEmitterHeight = 100; /* Create the particle emitter */ RectangleOutlineParticleEmitter particleEmitter = new RectangleOutlineParticleEmitter(particleSpawnCenterX, particleSpawnCenterY, particleEmitterWidth, particleEmitterHeight);
-
RectangleParticleEmitter
:这种粒子发射器子类型允许粒子在由构造函数参数定义的矩形形状的边界区域内任何位置生成。要测试这种粒子发射器子类型,只需将RectangleOutlineParticleEmitter
示例中的对象重构为使用RectangleParticleEmitter
对象即可。
粒子初始化器选择
粒子初始化器对粒子系统至关重要。它们为我们提供了对最初生成的每个单独粒子执行操作的可能性。这些粒子初始化器最棒的一点是,它们允许我们提供最小/最大值,这使我们有机会随机化生成粒子的属性。以下列出了 AndEngine 提供的所有粒子初始化器及其使用示例。请随意用此列表中的粒子初始化器替换配方中的那些。
注意
以下粒子初始化器可以通过简单的调用particleSystem.addParticleInitializer(pInitializer)
添加,此外,还可以通过particleSystem.removeParticleInitializer(pInitializer)
移除。
-
ExpireParticleInitializer
:我们将从列表中最必要的粒子初始化器开始。ExpireParticleInitializer
对象提供了一种移除存活时间过长的粒子的方法。如果我们不包括某种形式的粒子过期机制,那么随着所有粒子系统在任意给定时间都有可以激活的粒子数量的限制,我们的粒子很快就会没有粒子可以生成。以下示例创建了一个ExpireParticleModifier
对象,该对象使单个粒子在2
到4
秒之间过期:/* Define min/max particle expiration time */ final float minExpireTime = 2; final float maxExpireTime = 4; ExpireParticleInitializer<UncoloredSprite> expireParticleInitializer = new ExpireParticleInitializer<UncoloredSprite>(minExpireTime, maxExpireTime);
-
AccelerationParticleInitializer
:这个初始化器允许我们以加速度的形式应用移动,使得生成的粒子在达到定义的速度之前会加速。x 轴或 y 轴上的正值将使粒子向上向右移动,而负值将使粒子向下向左移动。在以下示例中,将为粒子赋予最小/最大值,这将导致粒子的移动方向是随机的:/* Define the acceleration values */ final float minAccelerationX = -25; final float maxAccelerationX = 25; final float minAccelerationY = 25; final float maxAccelerationY = 50; AccelerationParticleInitializer<UncoloredSprite> accelerationParticleInitializer = new AccelerationParticleInitializer<UncoloredSprite>(minAccelerationX, maxAccelerationX, minAccelerationY, maxAccelerationY);
-
AlphaInitializer
:AlphaInitializer
对象非常基础。它仅允许我们使用未确定的 alpha 值初始化粒子。以下示例将导致每个单独的粒子以0.5f
到1f
之间的 alpha 值生成:/* Define the alpha values */ final float minAlpha = 0.5f; final float maxAlpha = 1; AlphaParticleInitializer<UncoloredSprite> alphaParticleInitializer = new AlphaParticleInitializer<UncoloredSprite>(minAlpha, maxAlpha);
-
BlendFunctionParticleInitializer
:这个粒子初始化器允许我们生成应用了特定 OpenGL 混合函数的粒子。关于混合函数及其结果的更多信息,可以在网上找到许多资源。以下是使用BlendFunctionParticleInitializer
对象的示例:BlendFunctionParticleInitializer<UncoloredSprite> blendFunctionParticleInitializer = new BlendFunctionParticleInitializer<UncoloredSprite>(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
-
ColorParticleInitializer
:ColorParticleInitializer
对象允许我们为精灵提供最小/最大值之间的颜色。这使得我们可以随机化每个生成粒子的颜色。以下示例将生成具有完全不同随机颜色的粒子:/* Define min/max values for particle colors */ final float minRed = 0f; final float maxRed = 1f; final float minGreen = 0f; final float maxGreen = 1f; final float minBlue = 0f; final float maxBlue = 1f; ColorParticleInitializer<UncoloredSprite> colorParticleInitializer = new ColorParticleInitializer<UncoloredSprite>(minRed, maxRed, minGreen, maxGreen, minBlue, maxBlue);
-
GravityParticleInitializer
:这个粒子初始化器允许我们生成像遵循地球重力规则一样的粒子。GravityParticleInitializer
对象在其构造函数中不需要参数:GravityParticleInitializer<UncoloredSprite> gravityParticleInitializer = new GravityParticleInitializer<UncoloredSprite>();
-
RotationParticleInitializer
:RotationParticleInitializer
对象允许我们定义粒子生成时的旋转最小/最大值。以下示例将导致每个单独的粒子以0
到359
度之间的任意角度生成:/* Define min/max values for the particle's rotation */ final float minRotation = 0; final float maxRotation = 359; RotationParticleInitializer<UncoloredSprite> rotationParticleInitializer = new RotationParticleInitializer<UncoloredSprite>(minRotation, maxRotation);
-
ScaleParticleInitializer
:ScaleParticleInitializer
对象允许我们定义粒子生成时的缩放最小/最大值。以下示例将允许粒子以0.5f
到1.5f
之间的任意比例因子生成:/* Define min/max values for the particle's scale */ final float minScale = 0.5f; final float maxScale = 1.5f; ScaleParticleInitializer<UncoloredSprite> scaleParticleInitializer = new ScaleParticleInitializer<UncoloredSprite>(minScale, maxScale);
-
VelocityParticleInitializer
:这个最后的粒子初始化器,与AccelerationParticleInitializer
对象类似,允许我们在生成粒子时为它们提供移动。然而,这个初始化器使粒子以恒定速度移动,并且除非手动配置,否则不会随时间增加或减少速度:/* Define min/max velocity values of the particles */ final float minVelocityX = -25; final float maxVelocityX = 25; final float minVelocityY = 25; final float maxVelocityY = 50; VelocityParticleInitializer<UncoloredSprite> velocityParticleInitializer = new VelocityParticleInitializer<UncoloredSprite>(minVelocityX, maxVelocityX, minVelocityY, maxVelocityY);
有关 AndEngine 的粒子修改器列表,请参阅以下部分。
粒子修改器选择
AndEngine 的粒子修改器在开发复杂的粒子系统时非常有用。它们允许我们根据粒子存活的时间为单个粒子提供变化。与实体修改器类似,粒子修改器是“从时间到时间,从值到值”的格式。再次强调,请随意将列表中的任何粒子修改器添加到您当前测试项目中。
注意
以下粒子修改器可以通过简单的调用particleSystem.addParticleModifier(pModifier)
添加,并且可以通过particleSystem.removeParticleModifier(pModifier)
移除。
-
AlphaParticleModifier
:这个修改器允许粒子在其生命周期内,在两个时间点之间改变 alpha 值。以下示例中,修改器将在1
秒内从 alpha 值1
过渡到0
。修改器将在粒子生成后1
秒生效:/* Define the alpha modifier's properties */ final float fromTime = 1; final float toTime = 2; final float fromAlpha = 1; final float toAlpha = 0; AlphaParticleModifier<UncoloredSprite> alphaParticleModifier = new AlphaParticleModifier<UncoloredSprite>(fromTime, toTime, fromAlpha, toAlpha);
-
ColorParticleModifier
:这个修改器允许粒子在其生命周期内,在两个时间点之间改变颜色。以下修改器将导致粒子在两秒内从绿色变为红色,从时间0
开始。这意味着过渡将在粒子生成后立即开始:/* Define the color modifier's properties */ final float fromTime = 0; final float toTime = 2; final float fromRed = 0; final float toRed = 1; final float fromGreen = 1; final float toGreen = 0; final float fromBlue 0; final float toBlue = 0; ColorParticleModifier<UncoloredSprite> colorParticleModifier = new ColorParticleModifier<UncoloredSprite>(fromTime, toTime, fromRed, toRed, fromGreen, toGreen, fromBlue, toBlue);
-
OffCameraExpireParticleModifier
:将此修改器添加到粒子系统中,离开Camera
对象视野的粒子将被销毁。我们可以将此作为ExpireParticleInitializer
对象的替代,但任何粒子系统至少应该激活这两者之一。需要提供给这个修改器的唯一参数是我们的Camera
对象:OffCameraExpireParticleModifier<UncoloredSprite> offCameraExpireParticleModifier = new OffCameraExpireParticleModifier<UncoloredSprite>(mCamera);
-
RotationParticleModifier
:这个修改器允许我们在粒子的生命周期内,在两个时间点之间改变粒子的旋转角度。以下示例将导致粒子在其生命周期的1
到4
秒之间旋转180
度:/* Define the rotation modifier's properties */ final float fromTime = 1; final float toTime = 4; final float fromRotation = 0; final float toRotation = 180; RotationParticleModifier<UncoloredSprite> rotationParticleModifier = new RotationParticleModifier<UncoloredSprite>(fromTime, toTime, fromRotation, toRotation);
-
ScaleParticleModifier
:ScaleParticleModifier
对象允许我们在粒子的生命周期内,在两个时间点之间改变粒子的缩放比例。以下示例将导致粒子在其生命周期的1
到3
秒之间,从缩放比例0.5f
增长到1.5f
:/* Define the scale modifier's properties */ final float fromTime = 1; final float toTime = 3; final float fromScale = 0.5f; final float toScale = 1.5f; ScaleParticleModifier<UncoloredSprite> scaleParticleModifier = new ScaleParticleModifier<UncoloredSprite>(fromTime, toTime, fromScale, toScale);
-
IParticleModifier
:最后,我们有了粒子修改器接口,它允许我们在粒子初始化时或通过更新线程对每个粒子进行更新时,对单个粒子进行自定义修改。以下示例展示了如何通过在粒子到达Scene
对象坐标系下20
以下值时,禁用 y 轴上的移动来模拟粒子着陆。我们可以使用这个接口,根据需要虚拟地对粒子进行任何更改:IParticleModifier<UncoloredSprite> customParticleModifier = new IParticleModifier<UncoloredSprite>(){ /* Fired only once when a particle is first spawned */ @Override public void onInitializeParticle(Particle<UncoloredSprite> pParticle) { * Make customized modifications to a particle on initialization */ } /* Fired on every update to a particle in the particle system */ @Override public void onUpdateParticle(Particle<UncoloredSprite> pParticle) { * Make customized modifications to a particle on every update to the particle */ Entity entity = pParticle.getEntity(); * Obtain the particle's position and movement properties */ final float currentY = entity.getY(); final float currentVelocityY = pParticle.getPhysicsHandler().getVelocityY(); final float currentAccelerationY = pParticle.getPhysicsHandler().getAccelerationY(); /* If the particle is close to the bottom of the Scene and is moving... */ if(entity.getY() < 20 && currentVelocityY != 0 || currentAccelerationY != 0){ /* Restrict movement on the Y axis. Simulates landing on the ground */ pParticle.getPhysicsHandler().setVelocityY(0); pParticle.getPhysicsHandler().setAccelerationY(0); } } };
既然我们已经介绍了所有的粒子发射器、粒子初始化器和粒子修改器,尝试通过组合你想要的初始化器和修改器,创建更复杂的粒子系统吧!
另请参阅
-
在第一章,AndEngine 游戏结构中使用不同类型的纹理。
-
本章节中的了解 AndEngine 实体。
第三章:设计你的菜单
在本章中,我们将开始了解如何使用 AndEngine 创建一个易于管理的菜单系统。主题包括:
-
向菜单添加按钮
-
为菜单添加音乐
-
应用背景
-
使用视差背景创建透视效果
-
创建我们的关卡选择系统
-
隐藏和检索图层
引言
游戏中的菜单系统本质上是游戏提供的场景或活动的地图。在游戏中,菜单应该看起来吸引人,并微妙地提示在游戏过程中可以期待什么。菜单应该组织有序,便于玩家理解。在本章中,我们将看看我们可以应用到自己游戏中的各种选项,以创建适用于任何类型游戏的实用且吸引人的菜单。
向菜单添加按钮
在 AndEngine 中,我们可以使用任何Entity
对象或Entity
对象子类型创建触摸响应的按钮。然而,AndEngine 包含一个名为ButtonSprite
的类,其纹理表示取决于Entity
对象是被按下还是未被按下。在本教程中,我们将利用 AndEngine 的ButtonSprite
类并覆盖其onAreaTouched()
方法,以便向我们的菜单和/或游戏的Scene
对象添加触摸响应按钮。此外,本教程关于触摸事件的代码可以应用于游戏中的任何其他Entity
对象。
准备就绪…
本教程需要你对 AndEngine 中的Sprite
对象有基本的了解。请通读第一章中的使用不同类型的纹理教程,特别是关于图块纹理区域的部分。接下来,访问第二章中的通过精灵使场景生动教程,使用实体。
一旦涵盖了关于纹理和精灵的教程,请创建一个带有空的BaseGameActivity
类的新的 AndEngine 项目。最后,我们需要创建一个名为button_tiles.png
的精灵表,其中包含两个图像,并将其放置在项目中的assets/gfx/
文件夹中;一个用于“未按下”按钮的表示,另一个用于“按下”按钮的表示。请参考以下图片以了解图像应有的样子。以下图片是 300 x 50 像素,或者每个图块 150 x 50 像素:
请参考代码捆绑包中名为CreatingButtons
的类,并将代码导入到你的项目中。
如何操作…
ButtonSprite
类非常方便,因为它为我们处理了图块纹理区域与按钮状态变化之间的关系。以下步骤概述了设置ButtonSprite
对象所需执行的任务:
-
声明一个全局的
ITiledTextureRegion
对象,命名为mButtonTextureRegion
,然后在BaseGameActivity
类的onCreateResources()
方法中,创建一个新的适用于您的button_tiles.png
图像的BuildableBitmapTextureAtlas
对象。构建并加载纹理区域和纹理图集对象,以便我们稍后可以使用它们来创建ButtonSprite
对象。 -
为了使
ButtonSprite
对象按预期工作,我们应在mScene
对象上设置适当的触摸区域绑定。将以下代码复制到活动的onCreateScene()
方法中:mScene.setTouchAreaBindingOnActionDownEnabled(true);
-
创建
ButtonSprite
对象,为其提供mButtonTextureRegion
对象并重写其onAreaTouched()
方法:/* Create the buttonSprite object in the center of the Scene */ ButtonSprite buttonSprite = new ButtonSprite(WIDTH * 0.5f, HEIGHT * 0.5f, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()) { /* Override the onAreaTouched() event method */ @Override public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) { /* If buttonSprite is touched with the finger */ if(pSceneTouchEvent.isActionDown()){ /* When the button is pressed, we can create an event * In this case, we're simply displaying a quick toast */ CreatingButtons.this.runOnUiThread(new Runnable(){ @Override public void run() { Toast.makeText(getApplicationContext(), "Button Pressed!", Toast.LENGTH_SHORT).show(); } }); } /* In order to allow the ButtonSprite to swap tiled texture region * index on our buttonSprite object, we must return the super method */ return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX, pTouchAreaLocalY); } };
-
最后一步是注册触摸区域并将
buttonSprite
对象附加到mScene
对象:/* Register the buttonSprite as a 'touchable' Entity */ mScene.registerTouchArea(buttonSprite); /* Attach the buttonSprite to the Scene */ mScene.attachChild(buttonSprite);
它的工作原理是…
本食谱使用了ButtonSprite
对象与ITiledTextureRegion
对象来展示两个独立的按钮状态。其中一个图块将作为按钮未被按下时的纹理,另一个则作为当手指触摸显示上的Entity
对象时按钮被按下的纹理。
在第一步中,我们创建纹理资源以应用于ButtonSprite
对象,这将在接下来的步骤中实现。ButtonSprite
类需要一个具有两个索引的ITiledTextureRegion
对象,或者在本文档的入门... 部分所示的图中可以看到两个图块。ITiledTextureRegion
对象的第一索引应包含按钮未被按下的表示,这将默认应用于ButtonSprite
对象。第二个ITiledTextureRegion
索引应表示ButtonSprite
对象的按下状态。ButtonSprite
类会根据ButtonSprite
对象当前处于的状态自动在这两个ITiledTextureRegion
索引之间切换;分别是ButtonSprite.State.NORMAL
表示未被按下,将ButtonSprite
对象的当前图块索引设置为0
,以及ButtonSprite.State.PRESSED
,是的,你猜对了,表示按下状态,将ButtonSprite
对象的当前图块索引设置为1
。
在第二步中,为了让ButtonSprite
对象按预期工作,我们需要在mScene
对象内对按下动作启用触摸区域绑定。我们在活动生命周期的onCreateScene()
方法中启用此功能,在创建mScene
对象之后立即进行。这样做可以允许我们的ButtonSprite
对象在我们将手指拖离ButtonSprite
触摸区域时注册为未按下状态。如果忽略这一步,那么当我们将手指按在Entity
对象的触摸区域并拖离时,ButtonSprite
对象将保持按下状态,这对于玩家来说可能会被认为是“有缺陷”的。在第三步中,我们创建ButtonSprite
对象,并将其置于场景中心。理想情况下,我们可以创建ButtonSprite
对象并将其放置在场景上,它应该就能正常工作。然而,ButtonSprite
毕竟是一个按钮,因此当它被按下时应该触发一个事件。我们可以通过重写onAreaTouched()
超方法并根据ButtonSprite
对象的触摸区域是否被按下、手指是否在其上拖动或者手指是否从显示区域内释放来创建事件。在本教程中,我们仅在ButtonSprite
对象的pSceneTouchEvent
注册了isActionDown()
方法时显示一个Toast
消息。在游戏开发的真实场景中,这个按钮同样可以允许/禁止声音静音、开始新游戏,或者我们为其选择的任何其他动作。用于触摸事件状态检查的其他两个方法是pSceneTouchEvent.isActionMove()
和pSceneTouchEvent.isActionUp()
。
最后,一旦创建了buttonSprite
对象,我们将需要注册触摸区域并将Entity
对象附加到mScene
对象上。此时,我们应该清楚,为了在场景上显示一个实体,我们首先必须将其附加。同样,为了让buttonSprite
对象的onAreaTouched()
超方法能够执行,我们必须记得调用mScene.registerTouchArea(buttonSprite)
。对于任何我们希望提供触摸事件的其它Entity
对象也是如此。
另请参阅
-
在第一章中了解使用不同类型的纹理,AndEngine 游戏结构。
-
在第二章中了解AndEngine 实体,使用实体。
-
在第二章中了解如何通过精灵使场景生动,使用实体。
向菜单中添加音乐
在本主题中,我们将创建一个静音按钮,用于控制菜单主题音乐。按下静音按钮将导致音乐如果当前暂停则播放,如果当前播放则暂停。这种静音音乐和声音的方法也可以应用于游戏内的选项和其他允许声音和音乐播放的游戏区域。与之前的教程不同,我们将使用一个TiledSprite
对象,它允许我们根据声音是否播放或暂停来设置Sprite
对象的图块索引。请记住,这个教程不仅适用于启用和禁用菜单音乐。我们还可以在游戏过程中遵循同样的方法处理许多其他可切换的选项和状态。
准备工作…
本教程要求你对 AndEngine 中的Sprite
对象以及使用触摸事件执行操作有基本了解。此外,由于我们将在本教程中整合Music
对象,我们应当了解如何将Sound
和Music
对象加载到游戏资源中。请阅读整个教程,第一章中的处理不同类型的纹理,特别是关于图块纹理区域的部分。接下来,查看第一章中的AndEngine 游戏结构中的引入声音和音乐教程。最后,我们将处理精灵,因此我们应当快速浏览第二章中的使用精灵为场景注入生命教程。
在覆盖了纹理、声音和精灵的相关主题后,创建一个带有空的BaseGameActivity
类的新的 AndEngine 项目。我们需要创建一个名为sound_button_tiles.png
的精灵表,其中包含两个图像,并将其放置在项目的assets/gfx/
文件夹中;一个用于“非静音”按钮表示,另一个用于“静音”按钮表示。以下是一个图像的示例,以了解图像应该是什么样子。以下图像是 100 x 50 像素,或者每个图块 50 x 50 像素:
我们还需要在项目的assets/sfx/
文件夹中包含一个 MP3 格式的声音文件。声音文件可以是为你执行此教程目的而选择的任何喜欢的音乐曲目。
请参考代码包中名为MenuMusic
的类,并将代码导入到你的项目中。
如何操作…
本教程介绍了一系列 AndEngine 功能的组合。我们将音乐、纹理、精灵、图块纹理区域和触摸事件整合到一个便捷的小包中。结果是一个切换按钮,可以控制Music
对象的播放。按照以下步骤,看看我们是如何创建这个切换按钮的。
-
在第一步中,我们将使用两个全局对象;
mMenuMusic
是一个Music
对象,mButtonTextureRegion
是一个ITiledTextureRegion
对象。在活动的onCreateResources()
方法中,我们使用assets/*
文件夹中的相应资源创建这些对象。如果需要,请参考入门…部分提到的教程,了解更多关于创建这些资源的信息。 -
接下来,我们可以直接跳转到活动的
onPopulateScene()
方法,在这里我们将使用TiledSprite
类创建mMuteButton
对象。我们需要重写mMuteButton
对象的onAreaTouched()
方法,以便在按下按钮时暂停或播放音乐:/* Create the music mute/unmute button */ TiledSprite mMuteButton = new TiledSprite(buttonX, buttonY, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()) { /* Override the onAreaTouched() method allowing us to define custom * actions */ @Override public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) { /* In the event the mute button is pressed down on... */ if (pSceneTouchEvent.isActionDown()) { if (mMenuMusic.isPlaying()) { /* If music is playing, pause it and set tile index to MUTE */ this.setCurrentTileIndex(MUTE); mMenuMusic.pause(); } else { /* If music is paused, play it and set tile index to UNMUTE */ this.setCurrentTileIndex(UNMUTE); mMenuMusic.play(); } return true; } return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX, pTouchAreaLocalY); } };
-
创建按钮后,我们需要初始化
mMuteButton
和mMenuMusic
对象的初始状态。这一步包括将mMuteButton
对象的图块索引设置为UNMUTE
常量值,该值等于1
,注册并将mMuteButton
对象附加到mScene
对象,设置mMenuMusic
为循环播放,并最终在mMenuMusic
对象上调用play()
方法:/* Set the current tile index to unmuted on application startup */ mMuteButton.setCurrentTileIndex(UNMUTE); /* Register and attach the mMuteButton to the Scene */ mScene.registerTouchArea(mMuteButton); mScene.attachChild(mMuteButton); /* Set the mMenuMusic object to loop once it reaches the track's end */ mMenuMusic.setLooping(true); /* Play the mMenuMusic object */ mMenuMusic.play();
-
在处理任何
Music
对象时,最后一步是确保在应用最小化时暂停音乐,否则它将在后台继续播放。在本教程中,我们将最小化时暂停mMenuMusic
对象。然而,如果用户返回应用程序,只有当应用最小化时mMuteButton
对象的图块索引等于UNMUTE
常量值,音乐才会播放:@Override public synchronized void onResumeGame() { super.onResumeGame(); /* If the music and button have been created */ if (mMenuMusic != null && mMuteButton != null) { /* If the mMuteButton is set to unmuted on resume... */ if(mMuteButton.getCurrentTileIndex() == UNMUTE){ /* Play the menu music */ mMenuMusic.play(); } } } @Override public synchronized void onPauseGame() { super.onPauseGame(); /* Always pause the music on pause */ if(mMenuMusic != null && mMenuMusic.isPlaying()){ mMenuMusic.pause(); } }
它是如何工作的…
这个特定的教程在游戏开发中非常有用;不仅适用于声音和音乐的静音,还适用于各种切换按钮。虽然本教程专门处理Music
对象的播放,但它包含了开始使用各种其他切换按钮所需的所有必要代码,这些按钮可能更适合我们游戏的具体需求。
在第一步中,我们必须为mMenuMusic
对象和mMuteButton
对象设置必要的资源。mMenuMusic
对象将加载名为menu_music.mp3
的音频文件,该文件可以是任何 MP3 文件,最好是音乐轨道。mMuteButton
对象将加载名为sound_button_tiles.png
的图块表,其中包含两个单独的图块。这些对象都在BaseGameActivity
对象生命周期的onCreateResourceS()
方法中处理。关于这些资源的创建,可以在本教程的入门…部分提到的教程中找到更多信息。
在第二步中,我们设置了mMuteButton
对象,该对象属于TiledSprite
类型。TiledSprite
类允许我们使用ITiledTextureRegion
对象,这使得我们可以设置mMuteButton
对象将在场景中显示的当前图块索引。在重写的onAreaTouched()
方法中,我们通过if (pSceneTouchEvent.isActionDown())
语句检查mMuteButton
对象是否被按下。然后,我们通过Music
对象的isPlaying()
方法继续判断mMenuMusic
对象是否正在播放。如果音乐正在播放,那么在mMuteButton
按钮上按下手指将导致mMenuMusic
对象调用pause()
方法,并将mMuteButton
对象的当前图块索引恢复为MUTE
常量值,即0
。如果音乐没有播放,那么我们执行相反操作,在mMenuMusic
对象上调用play()
方法,并将mMuteButton
对象的图块索引恢复为UNMUTE
,即1
。
在第三步中,我们设置了mMenuMusic
和mMuteButton
对象的默认状态,即播放音乐并将当前图块索引设置为UNMUTE
。这将导致应用程序每次启动时播放音乐。设置好默认按钮和音乐状态后,我们继续注册mMuteButton
对象的触摸区域,并将Entity
对象附加到Scene
对象。这一步可以进一步操作,以保存mMuteButton
对象的状态到设备,从而根据用户过去的偏好加载音乐静音的默认状态。有关保存/加载数据和状态的信息,请参阅第一章中的保存和加载游戏数据菜谱,AndEngine 游戏结构。
最后一步非常重要,处理Music
对象时应该始终包含这一步。这一步的目的是在第一章中的引入声音和音乐菜谱中详细解释的。但是,这个菜谱在onResumeGame()
方法中的代码有一个小变化。在应用程序最小化的情况下,用户可能期望他们的游戏状态在等待,就像他们最后将其焦点返回时一样。因此,在应用程序最大化时触发onResumeGame()
时,我们不是播放mMenuMusic
对象,而是判断在游戏窗口最小化之前mMuteButton
按钮的图块索引是否设置为UNMUTE
。如果是这样,那么我们可以对mMenuMusic
对象调用play()
方法,否则我们可以忽略它,直到用户决定再次按下mMuteButton
播放音乐。
另请参阅
-
第一章中的处理不同类型的纹理,AndEngine 游戏结构。
-
在第一章,AndEngine 游戏结构中,引入声音和音乐。
-
在第二章,使用实体中,了解 AndEngine 实体。
-
在第二章,使用实体中,介绍如何通过精灵使场景生动起来。
应用背景
AndEngine 的Scene
对象允许我们为其应用静态背景。背景可以用来显示纯色、实体、精灵或重复精灵,这些都不会受到Camera
对象位置或缩放因子变化的影响。在本食谱中,我们将看看如何将不同类型的背景应用到我们的Scene
对象上。
如何操作…
在 AndEngine 中,Background
对象是我们Scene
对象最基本的背景类型。这个对象允许场景以纯色视觉展示。我们首先会设置Scene
对象以显示Background
对象,以便熟悉如何将背景应用到场景中。在本食谱的后面,我们将介绍大部分剩余的Background
对象子类型,以涵盖所有关于将背景应用到场景的选项。为Scene
对象设置背景只需以下两个步骤:
-
定义并创建
Background
对象的属性:/* Define background color values */ final float red = 0; final float green = 1; final float blue = 1; final float alpha = 1; /* Create a new background with the specified color values */ Background background = new Background(red, green, blue, alpha);
-
将
Background
对象设置到Scene
对象上,并启用背景功能:/* Set the mScene object's background */ mScene.setBackground(background); /* Set the background to be enabled */ mScene.setBackgroundEnabled(true);
工作原理…
在决定使用 AndEngine 的默认背景之前,我们必须确定背景是否需要考虑相机移动。我们可以将这些背景视为“粘附”在相机视图中。这意味着对相机所做的任何移动都不会影响背景的位置。对于任何其他形式的相机重新定位,包括缩放,也同样适用。因此,我们不应该在背景上包含任何需要随相机移动而滚动的对象。这是应用到Scene
对象的Background
对象与附加到Scene
对象的Entity
对象之间的区别。任何应该随相机移动而看似移动的“背景”,都应该作为Entity
对象附加到Scene
对象上,以作为“背景层”,所有表示背景图像的精灵都将附着在上面。
现在我们已经了解了Background
对象与Entity
对象之间的区别,接下来将继续介绍本食谱的步骤。从本食谱的步骤中我们可以看到,设置一个枯燥、老旧的有色背景是一项简单的任务。然而,了解它仍然是有用的。在第一步中,我们将定义Background
对象的属性,并创建一个Background
对象,将所述属性作为参数传入。对于基本的Background
对象,这些参数仅包括三个颜色值以及Background
对象颜色的 alpha 值。但是,正如我们稍后将讨论的,不同类型的背景将根据类型需要不同的参数。当我们讨论到这一点时,为了方便起见,将会概述这些差异。
在Scene
对象上设置Background
对象的第二步将始终是相同的两个方法调用,无论我们应用的是哪种类型的背景。我们必须通过setBackground(pBackground)
设置场景的背景,并通过调用setBackgroundEnabled(true)
确保场景的背景已启用。另一方面,我们也可以通过向后者方法提供一个false
参数来禁用背景。
这就是在我们的Scene
对象上设置背景的全部内容。然而,在我们自己的游戏中,我们很可能会对基本的有色背景感到不满意。请参阅本食谱的还有更多...部分,了解各种Background
对象子类型的列表和示例。
还有更多...
在以下各节中,我们将介绍我们可以在游戏中使用的不同类型的静态背景。所有的Background
对象子类型都允许我们为未被Sprite
实体、Rectangle
实体或其他方式覆盖的背景部分指定背景颜色。这些背景都遵循在工作原理...部分提到的相同"静态"规则,即它们在摄像头移动时不会移动。
EntityBackground 类
EntityBackground
类允许我们应用单个Entity
对象,或整个Entity
对象的图层作为我们场景的背景。这可以用于将多个Entity
对象组合到一个Background
对象中,以便在场景上显示。在以下代码中,我们将两个矩形附加到Entity
对象的图层上,然后使用Entity
对象作为背景:
/* Create a rectangle in the bottom left corner of the Scene */
Rectangle rectangleLeft = new Rectangle(100, 100, 200, 200,
mEngine.getVertexBufferObjectManager());
/* Create a rectangle in the top right corner of the Scene */
Rectangle rectangleRight = new Rectangle(WIDTH - 100, HEIGHT - 100, 200, 200,
mEngine.getVertexBufferObjectManager());
/* Create the entity to be used as a background */
Entity backgroundEntity = new Entity();
/* Attach the rectangles to the entity which will be applied as a background */
backgroundEntity.attachChild(rectangleLeft);
backgroundEntity.attachChild(rectangleRight);
/* Define the background color properties */
final float red = 0;
final float green = 0;
final float blue = 0;
/* Create the EntityBackground, specifying its background color & entity to represent the background image */
EntityBackground background = new EntityBackground(red, green, blue, backgroundEntity);
/* Set & enable the background */
mScene.setBackground(background);
mScene.setBackgroundEnabled(true);
EntityBackground
对象的参数包括red
、green
和blue
颜色值,最后是作为背景显示的Entity
对象或图层。一旦创建了EntityBackground
对象,我们只需按照本食谱中如何操作...部分的第二步进行操作,我们的EntityBackground
对象将准备好显示我们选择附加到backgroundEntity
对象上的任何内容!
SpriteBackground 类
SpriteBackground
类允许我们将单个Sprite
对象作为背景图像附加到场景中。请注意,为了适应显示的大小,这个精灵不会被拉伸或扭曲。为了使精灵在相机的视野中横跨整个宽度和高度,我们必须在考虑相机宽度和高度的情况下创建Sprite
对象。使用以下代码,我们可以将单个Sprite
对象作为场景的背景图像。假设mBackgroundTextureRegion
对象的尺寸与以下代码中的WIDTH
和HEIGHT
值相同,这些值表示相机的宽度和高度值:
/* Create the Sprite object */
Sprite sprite = new Sprite(WIDTH * 0.5f, HEIGHT * 0.5f, mBackgroundTextureRegion,
mEngine.getVertexBufferObjectManager());
/* Define the background color values */
final float red = 0;
final float green = 0;
final float blue = 0;
/* Create the SpriteBackground object, specifying
* the color values & Sprite object to display*/
SpriteBackground background = new SpriteBackground(red, green, blue, sprite);
/* Set & Enable the background */
mScene.setBackground(background);
mScene.setBackgroundEnabled(true);
我们可以像创建其他对象一样创建Sprite
对象。在创建SpriteBackground
对象时,我们传递常规颜色参数以及我们希望在背景上显示的Sprite
对象。
注意
当使用SpriteBackground
和RepeatingSpriteBackground
时,将BitmapTextureFormat.RGB_565
应用到纹理图集上是一个好主意。由于背景可能会横跨整个显示,我们通常不需要 alpha 通道,这可以提高在低端设备上游戏的性能。
RepeatingSpriteBackground 类
RepeatingSpriteBackground
类非常适合创建地形纹理图或仅仅用纹理填充场景中的空白空间。我们可以轻松地将以下 128 x 128 像素的纹理转换成背景,使其在整个显示长度上重复纹理:
使用前面纹理创建RepeatingSpriteBackground
对象后,得到的背景图像尺寸为 1280 x 752 像素,如下所示:
创建RepeatingSpriteBackground
对象需要比之前的Background
对象子类型多做一点工作。我们将重复的图像文件加载到AssetBitmapTexture
对象中,然后将其提取为ITextureRegion
对象供背景使用。由于我们要将纹理用于在RepeatingSpriteBackground
中重复,我们必须在AssetBitmapTexture
构造函数中提供TextureOptions.REPEATING_BILINEAR
或TextureOptions.REPEATING_NEAREST
纹理选项。此外,在处理重复纹理时,我们的图像文件尺寸必须保持为 2 的幂次方。OpenGL 的环绕模式要求纹理尺寸为 2 的幂次方,以正确地重复纹理。不遵循此规则将导致重复的精灵显示为黑色形状。将以下代码放入测试活动的onCreateResources()
方法中。mRepeatingTextureRegion
对象必须声明为全局ITextureRegion
对象:
AssetBitmapTexture mBitmapTexture = null;
try {
/* Create the AssetBitmapTexture with the REPEATING_* texture option */
mBitmapTexture = new AssetBitmapTexture(mEngine.getTextureManager(), this.getAssets(), "gfx/grass.png", BitmapTextureFormat.RGB_565,TextureOptions.REPEATING_BILINEAR);
} catch (IOException e) {
e.printStackTrace();
}
/* Load the bitmap texture */
mBitmapTexture.load();
/* Extract the bitmap texture into an ITextureRegion */
mRepeatingTextureRegion = TextureRegionFactory.extractFromTexture(mBitmapTexture);
下一步是创建RepeatingSpriteBackground
对象。我们将此代码包含在我们的活动生命周期的onCreateScene()
方法中:
/* Define the RepeatingSpriteBackground sizing parameters */
final float cameraWidth = WIDTH;
final float cameraHeight = HEIGHT;
final float repeatingScale = 1f;
/* Create the RepeatingSpriteBackground */
RepeatingSpriteBackground background = new RepeatingSpriteBackground(cameraWidth, cameraHeight, mRepeatingTextureRegion, repeatingScale,
mEngine.getVertexBufferObjectManager());
/* Set & Enable the background */
mScene.setBackground(background);
mScene.setBackgroundEnabled(true);
RepeatingSpriteBackground
对象的前两个参数定义了重复纹理将覆盖的最大区域,从显示的左下角开始。在本例中,我们覆盖了整个显示区域。我们传递的第三个纹理是作为重复纹理使用的ITextureRegion
对象。如前所述,这个纹理区域必须遵循二的幂次维度规则。第四个参数是重复纹理的缩放因子。默认缩放为1
;增加缩放会使重复纹理放大,这可能使重复模式更容易看到。减少缩放因子会缩小每个重复的纹理,有时可以帮助隐藏重复纹理中的明显瑕疵。请记住,调整重复纹理的缩放不会影响根据前两个参数定义的RepeatingSpriteBackground
对象的整体大小,因此可以自由调整,直到纹理看起来正确为止。
参见以下内容
-
在第一章,AndEngine 游戏结构中,使用不同类型的纹理。
-
在第二章,使用实体中,用精灵让场景生动起来。
使用视差背景创建透视效果
将视差背景应用于游戏可以产生视觉上令人愉悦的透视效果。尽管我们使用的是 2D 引擎,但我们可以创建一个通过使用视差值来产生深度错觉的背景,这些视差值根据相机移动确定精灵的运动速度。本主题将介绍视差背景以及如何使用它们为完全 2D 的世界添加深度感。我们将使用的类是ParallaxBackground
和AutoParallaxBackground
。
准备就绪…
此食谱需要具备 AndEngine 中Sprite
对象的基本知识。请通读第一章,AndEngine 游戏结构中的整个食谱,使用不同类型的纹理。接下来,请访问第二章,使用实体中的食谱,用精灵让场景生动起来。
在介绍了纹理和精灵的相关方法之后,创建一个带有空的BaseGameActivity
类的新的 AndEngine 项目。最后,我们需要在项目的assets/gfx/
文件夹中创建一个名为hill.png
的图像。这个图像的尺寸应为 800 x 150 像素。图像可以类似于以下图形:
请参考代码包中名为UsingParallaxBackgrounds
的类,并将代码导入到您的项目中。
如何操作…
ParallaxBackground
对象是 AndEngine 中最先进的Background
对象子类型。它需要比所有Background
对象子类型更多的设置,但如果分解成小步骤,实际上并不那么困难。执行以下步骤,了解如何设置一个与相机移动相关的ParallaxBackground
对象。为了简洁起见,我们将省略可以在活动生命周期的onCreateEngineOptions()
方法中找到的自动相机移动代码:
-
创建
Sprite
对象的ITextureRegion
对象的第一个步骤通常是创建我们的BuildableBitmapTextureAtlas
。纹理图集应足够大以容纳hill.png
图像,其宽度为 800 像素,高度为 150 像素。创建纹理图集后,继续创建ITextureRegion
对象,然后像往常一样构建和加载纹理图集。这应该都在活动生命周期的onCreateResources()
方法内完成。 -
剩余的步骤将在活动生命周期的
onCreateScene()
方法内进行。首先,我们需要创建所有将出现在背景上的Sprite
对象。在此教程中,我们将应用三个Sprite
对象,以便方便地放置在背景上,以增强不同精灵之间的距离错觉:final float textureHeight = mHillTextureRegion.getHeight(); /* Create the hill which will appear to be the furthest * into the distance. This Sprite will be placed higher than the * rest in order to retain visibility of it */ Sprite hillFurthest = new Sprite(WIDTH * 0.5f, textureHeight * 0.5f + 50, mHillTextureRegion, mEngine.getVertexBufferObjectManager()); /* Create the hill which will appear between the furthest and closest * hills. This Sprite will be placed higher than the closest hill, but * lower than the furthest hill in order to retain visibility */ Sprite hillMid = new Sprite(WIDTH * 0.5f, textureHeight * 0.5f + 25, mHillTextureRegion, mEngine.getVertexBufferObjectManager()); /* Create the closest hill which will not be obstructed by any other hill * Sprites. This Sprite will be placed at the bottom of the Scene since * nothing will be covering its view */ Sprite hillClosest = new Sprite(WIDTH * 0.5f, textureHeight * 0.5f, mHillTextureRegion, mEngine.getVertexBufferObjectManager());
-
接下来,我们将创建
ParallaxBackground
对象。构造函数的三个参数通常定义背景颜色。更重要的是,我们必须重写ParallaxBackground
对象的onUpdate()
方法,以处理在背景上等待任何相机移动时Sprite
对象移动:/* Create the ParallaxBackground, setting the color values to represent * a blue sky */ ParallaxBackground background = new ParallaxBackground(0.3f, 0.3f, 0.9f) { /* We'll use these values to calculate the parallax value of the background */ float cameraPreviousX = 0; float parallaxValueOffset = 0; /* onUpdates to the background, we need to calculate new * parallax values in order to apply movement to the background * objects (the hills in this case) */ @Override public void onUpdate(float pSecondsElapsed) { /* Obtain the camera's current center X value */ final float cameraCurrentX = mCamera.getCenterX(); /* If the camera's position has changed since last * update... */ if (cameraPreviousX != cameraCurrentX) { /* Calculate the new parallax value offset by * subtracting the previous update's camera x coordinate * from the current update's camera x coordinate */ parallaxValueOffset += cameraCurrentX - cameraPreviousX; /* Apply the parallax value offset to the background, which * will in-turn offset the positions of entities attached * to the background */ this.setParallaxValue(parallaxValueOffset); /* Update the previous camera X since we're finished with this * update */ cameraPreviousX = cameraCurrentX; } super.onUpdate(pSecondsElapsed); } };
-
创建
ParallaxBackground
对象后,我们现在必须将ParallaxEntity
对象附加到ParallaxBackground
对象上。ParallaxEntity
对象要求我们为实体定义一个视差因子以及一个用于视觉表示的Sprite
对象,在这种情况下将是山丘:background.attachParallaxEntity(new ParallaxEntity(5, hillFurthest)); background.attachParallaxEntity(new ParallaxEntity(10, hillMid)); background.attachParallaxEntity(new ParallaxEntity(15, hillClosest));
-
最后,像所有
Background
对象一样,我们必须将其应用到Scene
对象并启用它:/* Set & Enabled the background */ mScene.setBackground(background); mScene.setBackgroundEnabled(true);
它是如何工作的…
在此教程中,我们将设置一个ParallaxBackground
对象,其中包含三个独立的ParallaxEntity
对象。这三个ParallaxEntity
对象将代表我们场景背景中的山丘。通过使用视差因子和视差值,ParallaxBackground
对象允许每个ParallaxEntity
对象在Camera
对象改变其位置时以不同的速度偏移它们的位置。这使得ParallaxEntity
对象能够产生透视效果。众所周知,离我们更近的物体会比远处的物体看起来移动得更快。
在“如何操作...”部分的第一步是创建我们的Sprite
对象的基本且必要的任务。在这个食谱中,我们使用一个单一的纹理区域/图像来表示所有三个将附加到背景的精灵。然而,请随意修改这个食谱,以便让三个Sprite
对象中的每一个都能使用自己的定制图像。实践将有助于进一步理解如何操作ParallaxBackground
对象,在游戏中创建整洁的场景。
在第二步中,我们设置三个将作为ParallaxEntity
对象附加到背景的Sprite
对象。我们将它们都放置在场景中心的 x 坐标处。ParallaxBackground
类仅用于将透视应用于 x 坐标移动,因此,随着摄像机的移动,背景上的精灵位置将离开初始 x 坐标。也就是说,重要的是要知道ParallaxBackground
对象将不断地将附加到背景的每个ParallaxEntity
对象的副本拼接在一起,以补偿可能离开摄像机视野的背景对象。以下是ParallaxBackground
对象如何将背景对象端对端拼接的可视化表示:
由于ParallaxEntity
对象在ParallaxBackground
对象上的拼接方式,为了创建可能不会在背景上经常出现的对象,我们必须在图像文件本身中包含透明填充。
至于定义精灵的 y 坐标,最好是将精灵分散开,以便能够区分背景上最近和最远的山丘。为了创建最佳的透视效果,最远的物体在场景中应该显得更高,因为从层次上讲,它们将隐藏在更近的对象后面。
在第三步中,我们创建ParallaxBackground
对象。构造函数与所有其他Background
对象子类型一样,定义了背景颜色。真正的魔法发生在ParallaxBackground
对象的覆盖onUpdate()
方法中。我们有两个变量;cameraPreviousX
和cameraCurrentX
,它们将首先被测试以确保两者之间存在差异,以减少任何不必要的代码执行。如果这两个值不相等,我们将累积先前和当前摄像机位置之间的差异到一个parallaxValueOffset
变量中。通过在ParallaxBackground
对象上调用setParallaxValue(parallaxValueOffset)
,我们基本上只是告诉背景摄像机已经改变了位置,现在是更新所有ParallaxEntity
对象位置以进行补偿的时候了。增加视差值将导致ParallaxEntity
对象向左平移,而减少它则导致它们向右平移。
在第四步中,我们最终创建ParallaxEntity
对象,为每个对象提供一个视差因子和一个Sprite
对象。视差因子将定义Sprite
对象基于摄像头移动的速度是快还是慢。为了创建更逼真的风景,距离较远的对象应该具有比近处对象更小的值。此外,attachParallaxEntity(pParallaxEntity)
方法类似于将Entity
对象附加到Scene
对象,因为第二个附加的对象将出现在第一个前面,第三个将出现在第二个前面,依此类推。因此,我们从最远的对象开始将ParallaxEntity
对象附加到ParallaxBackground
,然后逐步靠近最近的物体。
完成所有前面的步骤后,我们可以简单地将ParallaxBackground
应用到Scene
对象并启用它。从现在开始,任何摄像头的移动都将决定背景景物中对象的位置!
还有更多…
AndEngine 还包括一个AutoParallaxBackground
类,它和ParallaxBackground
类在设置视觉效果方面类似。两者的区别在于,AutoParallaxBackground
类允许我们指定一个恒定速率,在该速率下,无论摄像头是否移动,ParallaxEntity
对象都会在屏幕上移动。这种类型的背景对于需要看起来不断移动的游戏很有用,比如赛车游戏或任何快节奏的横版滚动游戏。另一方面,AutoParallaxBackground
类也可以用于在游戏过程中创建简单的效果,例如云层在屏幕上持续滚动,即使是在Camera
和Scene
对象位置看似静态的游戏中也是如此。
我们可以通过对这一食谱活动的简单调整来创建一个AutoParallaxBackground
对象。用以下代码片段替换当前的ParallaxBackground
对象创建。注意,autoParallaxSpeed
变量定义了ParallaxEntity
对象在背景上的移动速度,因为它们不再基于摄像头的移动:
/* Define the speed that the parallax entities will move at.
*
* Set to a negative value for movement in the opposite direction */
final float autoParallaxSpeed = 3;
/* Create an AutoParallaxBackground */
AutoParallaxBackground background = new AutoParallaxBackground(0.3f, 0.3f, 0.9f, autoParallaxSpeed);
此外,移除所有与mCamera
对象的onUpdate()
方法相关的代码,因为它将不再影响ParallaxEntity
对象的位置。
下图展示了将三个不同高度的丘陵层附加到ParallaxBackground
或AutoParallaxBackground
对象的结果,当然,这里没有考虑移动:
另请参阅
-
在第一章《AndEngine 游戏结构》中处理不同类型的纹理。
-
在第二章《使用实体》中用精灵为场景赋予生命。
-
本章节提供的应用背景。
创建我们的关卡选择系统
如果你曾经玩过带有多个关卡的移动游戏,那么你可能已经知道我们将在本章中处理什么。我们将创建一个类,为游戏提供一个包含关卡瓦片的网格,以便用户可以选择一个关卡进行游戏。这个类非常易于管理,并且高度可定制,从按钮纹理、列数、行数等,都可以轻松设置。最终结果将如下所示:
注意
LevelSelector
类的这个实现扩展了 AndEngine 的Entity
对象。这使得应用实体修改器和基于触摸事件的滚动变得相当简单。
准备就绪…
LevelSelector
类高度依赖于 AndEngine 的Entity
、Sprite
和Text
对象的使用。为了理解LevelSelector
是如何工作的,请花时间阅读关于这些对象的相关内容。这些内容包括第二章中的理解 AndEngine 实体,使用实体,用精灵使场景生动,以及将文本应用到图层。
LevelSelector
对象需要一个带有图像文件引用的ITextureRegion
对象。可以自由创建一个表示 50x50 像素尺寸的方形按钮的图像,如本食谱介绍中的图所示。虽然这个ITextureRegion
对象在LevelSelector
类内部并不需要,但在本食谱末尾在空的BaseGameActivity
测试项目中测试LevelSelector
类时需要它。
请参考代码包中名为LevelSelector
的类,以获取此食谱的工作代码。请随意使用这个类,并根据你的游戏需求进行修改!
如何操作…
尽管其规模可能相当大,但LevelSelector
类实际上非常易于使用。在这个食谱中,我们将介绍两个类;第一个是处理关卡瓦片(或按钮)如何在场景上形成网格的LevelSelector
类。第二个是LevelSelector
的内部类,称为LevelTile
。LevelTile
类允许我们轻松添加或删除可能对游戏有用的额外数据。为了保持简单,我们将分别讨论这两个类,从LevelSelector
类开始。
以下步骤解释了LevelSelector
类如何以网格格式在场景上排列LevelTile
对象:
-
创建
LevelSelector
构造函数,初始化所有变量。这个构造函数很直接,直到我们必须通过mInitialX
和mInitialY
变量指定第一个LevelTile
对象的位置:final float halfLevelSelectorWidth = ((TILE_DIMENSION * COLUMNS) + TILE_PADDING * (COLUMNS - 1)) * 0.5f; this.mInitialX = (this.mCameraWidth * 0.5f) - halfLevelSelectorWidth; /* Same math as above applies to the Y coordinate */ final float halfLevelSelectorHeight = ((TILE_DIMENSION * ROWS) + TILE_PADDING * (ROWS - 1)) * 0.5f; this.mInitialY = (this.mCameraHeight * 0.5f) + halfLevelSelectorHeight;
-
接下来,我们必须创建一个方法,用于构建
LevelSelector
对象的瓦片网格。我们正在创建一个名为createTiles(pTextureRegion, pFont)
的方法,通过循环一定的ROWS
和COLUMNS
值,将瓦片放置在预定的坐标中,从而完全自动化创建关卡瓦片网格:public void createTiles(final ITextureRegion pTextureRegion, final Font pFont) { /* Temp coordinates for placing level tiles */ float tempX = this.mInitialX + TILE_DIMENSION * 0.5f; float tempY = this.mInitialY - TILE_DIMENSION * 0.5f; /* Current level of the tile to be placed */ int currentTileLevel = 1; /* Loop through the Rows, adjusting tempY coordinate after each * iteration */ for (int i = 0; i < ROWS; i++) { /* Loop through the column positions, placing a LevelTile in each * column */ for (int o = 0; o < COLUMNS; o++) { final boolean locked; /* Determine whether the current tile is locked or not */ if (currentTileLevel <= mMaxLevel) { locked = false; } else { locked = true; } /* Create a level tile */ LevelTile levelTile = new LevelTile(tempX, tempY, locked, currentTileLevel, pTextureRegion, pFont); /* Attach the level tile's text based on the locked and * currentTileLevel variables pass to its constructor */ levelTile.attachText(); /* Register & Attach the levelTile object to the LevelSelector */ mScene.registerTouchArea(levelTile); this.attachChild(levelTile); /* Increment the tempX coordinate to the next column */ tempX = tempX + TILE_DIMENSION + TILE_PADDING; /* Increment the level tile count */ currentTileLevel++; } /* Reposition the tempX coordinate back to the first row (far left) */ tempX = mInitialX + TILE_DIMENSION * 0.5f; /* Reposition the tempY coordinate for the next row to apply tiles */ tempY = tempY - TILE_DIMENSION - TILE_PADDING; } }
-
LevelSelector
类的第三步也是最后一步是包含两个方法;一个用于显示LevelSelector
类的网格,另一个用于隐藏LevelSelector
类的网格。为了简单起见,我们将这些方法称为show()
和hide()
,不带参数:/* Display the LevelSelector on the Scene. */ public void show() { /* Register as non-hidden, allowing touch events */ mHidden = false; /* Attach the LevelSelector the the Scene if it currently has no parent */ if (!this.hasParent()) { mScene.attachChild(this); } /* Set the LevelSelector to visible */ this.setVisible(true); } /* Hide the LevelSelector on the Scene. */ public void hide() { /* Register as hidden, disallowing touch events */ mHidden = true; /* Remove the LevelSelector from view */ this.setVisible(false); }
现在,我们继续讨论LevelTile
类的步骤。LevelTile
内部类是 AndEngine 的Sprite
对象的扩展。我们实现自己的LevelTile
类的原因是让每个瓦片存储自己的数据,例如瓦片表示的关卡是否锁定,用于显示瓦片关卡编号的Font
和Text
对象,瓦片的关卡编号本身等等。这个类可以很容易地被修改以存储更多信息,例如特定关卡的用户的最高分,关卡颜色主题,或者我们想要包含的任何其他内容。以下步骤将引导我们创建LevelTile
内部类:
-
创建
LevelTile
构造函数:public LevelTile(float pX, float pY, boolean pIsLocked, int pLevelNumber, ITextureRegion pTextureRegion, Font pFont) { super(pX, pY, LevelSelector.this.TILE_DIMENSION, LevelSelector.this.TILE_DIMENSION, pTextureRegion, LevelSelector.this.mEngine.getVertexBufferObjectManager()); /* Initialize the necessary variables for the LevelTile */ this.mFont = pFont; this.mIsLocked = pIsLocked; this.mLevelNumber = pLevelNumber; }
-
为
LevelTile
类创建必要的 getter 方法。对于这样一个基本的LevelTile
类,我们只需要访问有关瓦片表示的关卡编号的锁定状态以及瓦片表示的关卡编号的数据:/* Method used to obtain whether or not this level tile represents a * level which is currently locked */ public boolean isLocked() { return this.mIsLocked; } /* Method used to obtain this specific level tiles level number */ public int getLevelNumber() { return this.mLevelNumber; }
-
为了在每个
LevelTile
对象上显示关卡编号,我们将创建一个attachText()
方法,以在创建每个LevelTile
对象后处理将Text
对象应用到它们上面:public void attachText() { String tileTextString = null; /* If the tile's text is currently null... */ if (this.mTileText == null) { /* Determine the tile's string based on whether it's locked or * not */ if (this.mIsLocked) { tileTextString = "Locked"; } else { tileTextString = String.valueOf(this.mLevelNumber); } /* Setup the text position to be placed in the center of the tile */ final float textPositionX = LevelSelector.this.TILE_DIMENSION * 0.5f; final float textPositionY = textPositionX; /* Create the tile's text in the center of the tile */ this.mTileText = new Text( textPositionX, textPositionY, this.mFont, tileTextString, tileTextString.length(), LevelSelector.this.mEngine.getVertexBufferObjectManager()); /* Attach the Text to the LevelTile */ this.attachChild(mTileText); } }
-
最后但同样重要的是,我们将重写
LevelTile
类的onAreaTouched()
方法,以便在瓦片被按下时提供一个默认操作。执行的的事件应根据mIsLocked
布尔值的不同而有所不同:@Override public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) { /* If the LevelSelector is not hidden, proceed to execute the touch * event */ if (!LevelSelector.this.mHidden) { /* If a level tile is initially pressed down on */ if (pSceneTouchEvent.isActionDown()) { /* If this level tile is locked... */ if (this.mIsLocked) { /* Tile Locked event... */ LevelSelector.this.mScene.getBackground().setColor( org.andengine.util.adt.color.Color.RED); } else { /* Tile unlocked event... This event would likely prompt * level loading but without getting too complicated we * will simply set the Scene's background color to green */ LevelSelector.this.mScene.getBackground().setColor( org.andengine.util.adt.color.Color.GREEN); /** * Example level loading: * LevelSelector.this.hide(); * SceneManager.loadLevel(this.mLevelNumber); */ } return true; } } return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX, pTouchAreaLocalY); }
它是如何工作的…
这种LevelSelector
类的实现允许我们通过在活动中添加少量的代码来创建可选择关卡瓦片的网格。在我们讨论将LevelSelector
类实现到我们的活动中之前,让我们看看这个类是如何工作的,以便我们了解如何可能修改这个类以更好地满足一系列不同游戏的具体需求。正如如何做…部分根据这个食谱中使用的两个类将步骤分为两段一样,我们也将分两个部分解释每个类是如何工作的。我们将再次从LevelSelector
类开始。
解释LevelSelector
类
首先,LevelSelector
类包含了许多成员变量,我们需要了解这些变量,才能充分利用这个对象。以下是在此类中使用变量的列表以及每个变量的描述:
-
COLUMNS
:LevelSelector
类网格水平轴上显示的LevelTile
对象数量。 -
ROWS
:LevelSelector
类网格垂直轴上显示的LevelTile
对象数量。 -
TILE_DIMENSION
:每个单独的LevelTile
对象的宽度和高度值。 -
TILE_PADDING
:LevelSelector
类网格上每个LevelTile
对象之间的间距(以像素为单位)。 -
mChapter
:此值定义了LevelSelector
类的章节值。这个变量可以让我们通过为每个LevelSelector
对象指定不同的章节值,创建代表游戏内不同章节/世界/区域的一系列LevelSelector
对象。 -
mMaxLevel
:此值定义了用户在我们游戏中当前已达到的最高解锁等级。这个变量将会与每个被触碰的LevelTile
对象的等级数字进行测试。不应该允许用户进入大于此变量的等级。 -
mCameraWidth
/mCameraHeight
:这些值仅用于帮助将LevelSelector
和LevelTile
对象正确对齐在场景中心。 -
mInitialX
:此变量用于保存LevelSelector
类网格每一行的初始 x 坐标的引用。每次网格的一整行布局完成后,下一行的第一个LevelTile
对象将返回到这个 x 坐标。 -
mInitialY
:此变量仅用于定义第一个LevelTile
对象的 y 坐标。由于我们是按照从左到右、从上到下的方式构建LevelSelector
类的网格,因此在后续的瓷砖放置中,我们无需返回到初始的 y 坐标。 -
mHidden
:此变量的布尔值确定LevelTile
对象是否响应触摸事件。如果LevelSelector
对象在场景中不可见,此变量设置为true
,否则为false
。
所有成员变量都处理完毕后,理解LevelSelector
类的工作原理就会变得轻而易举!在第一步中,我们创建LevelSelector
构造函数以初始化所有类变量。构造函数应该很容易理解,直到我们定义mInitialX
和mInitialY
变量的那一点。我们所做的就是基于列数、行数、瓦片尺寸和瓦片间隔来计算LevelSelector
类网格的整体宽度和高度的一半。为了计算总宽度,我们需要将COLUMNS
值乘以每个LevelTile
对象的宽度。由于我们在每个瓦片之间包括间隔,我们还必须计算间隔将占用的空间。然而,间隔只会在瓦片之间发生,这意味着在最后一列不需要计算间隔,因此我们可以从间隔计算中减去一列。然后我们将这个值除以一半,以得出整个网格宽度的一半。最后,从Camera
对象的中心位置减去整个网格宽度的一半,将给我们第一个LevelTile
对象的 x 坐标!同样的数学方法适用于计算初始 y 坐标,除了 y 轴处理行而不是列,因此我们需要在计算mInitialY
变量时进行相应的调整,以获得正确的 y 坐标。
LevelSelector
类的第二步介绍了LevelTile
对象创建和放置的方法。这是网格制作的魔法开始的地方。在我们开始迭代之前,我们声明并定义了临时的坐标,这些坐标将用于在网格上放置每个LevelTile
对象,并在放置每个瓦片后相应地增加它们的值。TILE_DIMENSION * 0.5f
的计算仅仅是为了适应 AndEngine 的Entity
对象的锚点,或者说是依赖于Entity
对象中心的放置坐标。此外,我们初始化了一个名为currentTileLevel
的临时关卡数,将其初始化为1
,这表示第一关的瓦片。每次在网格上放置一个关卡瓦片时,这个变量都会增加 1。定义了初始关卡瓦片的值后,我们继续创建for
循环,它将遍历构成网格的行和列的每个位置。从第一行开始,我们将遍历 N 列,每次放置瓦片后,通过加上TILE_DIMENSION
和TILE_PADDING
来增加tempX
变量,这将给我们下一个位置。当我们达到最大列数时,我们通过加上TILE_DIMENSION
和TILE_PADDING
来减少tempY
变量,以便下降到下一行进行填充。这个过程一直持续到没有更多的行需要填充。
LevelSelector
类中最后一步包括调用setVisible(pBoolean)
的代码,在LevelSelector
对象上设置,如果调用show()
方法则启用可见性,如果调用hide()
方法则禁用可见性。第一次LevelSelector
对象调用show()
时,它将被附加到Scene
对象上。此外,mHidden
变量将根据LevelSelector
对象的可见性进行调整。
解释LevelTile
类。
与LevelSelector
类一样,我们将从概述LevelTile
类不同成员变量的目的开始。以下是此类别中使用的变量列表以及每个变量的描述:
-
mIsLocked
:mIsLocked
布尔变量由LevelTile
构造函数中的参数定义。此变量定义了此LevelTile
对象的触摸事件是否应该产生积极事件,如加载关卡,或消极事件,如通知关卡已锁定。 -
mLevelNumber
:这个变量简单地保存了LevelTile
对象级别编号的值。该值是根据其在网格上的位置确定的;例如,放置在网格上的第一个瓦片将代表第 1 关,第二个瓦片将代表第 2 关,依此类推。 -
mFont
和mTileText
:mFont
和mTileText
对象用于在每个LevelTile
上显示Text
对象。如果LevelTile
对象被认为是锁定的,那么瓦片上将会显示单词locked,否则将显示瓦片的关卡编号。
在LevelTile
类的第一步中,我们只是介绍了构造函数。这里没有什么特别之处。但需要注意的是,构造函数确实依赖于常量TILE_DIMENSION
值来指定瓦片的宽度/高度尺寸,而不需要指定参数。这是为了保持LevelSelector
和LevelTile
类之间的一致性。
在第二步中,我们引入了两个 getter 方法,可以用来获取LevelTile
类的更重要值。尽管我们目前在任何一个类中都没有使用这些方法,但当LevelSelector
/LevelTile
对象被实现到一个需要如关卡编号等数据在游戏中传递的全功能游戏中时,它们可能变得很重要。
第三步介绍了一种方法,用于将Text
对象附加到LevelTile
,称为attachText()
。此方法将mTileText
对象放置在LevelTile
对象的正中心,其字符串取决于LevelTile
对象的锁定状态。如mFont
和mTileText
变量解释中所述,mTileText
对象的String
变量将显示locked(锁定)或瓦片的关卡编号。
最后一步要求我们覆盖LevelTile
对象的onAreaTouched()
方法。在我们考虑对任何瓷砖上的触摸事件做出响应之前,我们首先要确定包含LevelTile
对象的LevelSelector
对象是否可见。如果不可见,就没有必要处理任何触摸事件;但如果LevelSelector
对象可见,那么我们就继续检查瓷砖是否被按下。如果按下了LevelTile
对象,我们接着检查瓷砖是锁定还是解锁。在类的当前状态下,我们只是设置场景背景的颜色,以表示按下的瓷砖是否锁定。然而,在实际应用中,当前锁定事件可以替换为基本通知,表明选定的瓷砖已锁定。如果瓷砖没有锁定,那么触摸事件应该根据LevelTile
对象的mLevelNumber
变量将用户带到选定的关卡。如果游戏包含多个章节/世界/区域,那么我们可以根据游戏加载关卡的方式,采用以下伪代码实现:
LevelSelector.this.hide();
SceneManager.loadLevel(this.mLevelNumber, LevelSelector.this.mChapter);
还有更多…
一旦我们将LevelSelector
类包含在我们选择的任何项目中,我们就可以轻松地将工作级别的选择网格实现到我们的BaseGameActivity
中。为了正确创建LevelSelector
对象并在我们的场景中显示它,我们需要确保已经创建了ITextureRegion
对象和字体对象,以便在为LevelSelector
类创建LevelTile
对象时使用。我们将省略资源创建代码,以保持LevelSelector
类的示例简洁。如有需要,请访问第一章中的食谱,处理不同类型的纹理以及使用 AndEngine 字体资源,了解更多关于如何为这个类设置必要资源的信息。
下面的代码展示了如何创建LevelSelector
对象,可以在创建必要的ITextureRegion
和字体objects
之前,将其复制到任何活动的onCreateScene()
方法中:
/* Define the level selector properties */
final int maxUnlockedLevel = 7;
final int levelSelectorChapter = 1;
final int cameraWidth = WIDTH;
final int cameraHeight = HEIGHT
/* Create a new level selector */
LevelSelector levelSelector = new LevelSelector(maxUnlockedLevel, levelSelectorChapter, cameraWidth, cameraHeight, mScene, mEngine);
/* Generate the level tiles for the levelSelector object */
levelSelector.createTiles(mTextureRegion, mFont);
/* Display the levelSelector object on the scene */
levelSelector.show();
这个LevelSelector
类的一个很好的特性是它是一个Entity
对象子类型。如果我们希望对其应用花哨的过渡效果,以便根据需要进出摄像头的视野,我们可以简单地调用levelSelector.registerEntityModifier(pEntityModifier)
。由于在调用createTiles()
方法时,LevelTile
对象附加到LevelSelector
对象上,因此LevelSelector
对象位置的任何变化也会同步影响所有LevelTile
对象。这也使得在处理多个章节时,创建可滚动的关卡选择器实现变得非常容易添加。
参见
-
在第二章中了解AndEngine 实体,使用实体。
-
在第二章中,通过精灵使场景生动起来,使用实体。
-
在第二章中将文本应用到图层中,使用实体。
隐藏和检索图层
在我们的游戏中,屏幕管理有几个不同的选项;屏幕可以是菜单屏幕、加载屏幕、游戏玩法屏幕等等。我们可以使用多个活动来充当每个屏幕,我们可以使用更明显的Scene
对象来充当游戏中的每个屏幕,或者我们可以使用Entity
对象来充当每个屏幕。尽管大多数开发者倾向于跟随使用多个活动或多个Scene
对象来充当不同的游戏屏幕,但我们将快速查看使用Entity
对象来充当游戏中的不同屏幕。
使用Entity
对象作为我们游戏中的各种屏幕,相较于前述两种方法有许多好处。实体方法允许我们同时向游戏中应用许多不同的屏幕或图层。与使用多个活动或Scene
对象作为游戏中的不同屏幕不同,我们可以使用Entity
对象在设备上可视化显示多个屏幕。这非常有用,因为我们可以应用进入或离开游戏玩法时的过渡效果,并根据需要轻松加载和卸载资源。
下面的图片展示了此配方代码的实际应用。我们看到的是两个带有多个Rectangle
子对象的Entity
图层,在相机的视野中交替进行过渡进入和过渡移出。这表示我们可以如何使用Entity
对象来处理一组或多组子对象之间的过渡效果:
准备就绪…
此配方需要了解Entity
对象以及它们如何被用作图层来包含一组子对象。此外,我们通过使用实体修改器为这些图层添加过渡效果。在继续此配方之前,请确保阅读第二章中的整个配方,了解 AndEngine 实体,使用实体,覆盖 onManagedUpdate() 方法,以及使用修改器和实体修改器。
请参考代码包中名为HidingAndRetrievingLayers
的类,以获取此配方的有效代码,并将其导入一个空的 AndEngine BaseGameActivity
类。
如何操作…
以下步骤概述了如何使用实体修改器来处理游戏内不同屏幕/层次之间的过渡效果。这个食谱包括一个处理层次转换的简单方法,然而在实际应用中,这项任务通常是由屏幕/层次管理类完成的。层次之间的交换是基于经过的时间,仅用于自动化演示的目的。
-
创建并将层次/屏幕定义为
Entity
对象,以及使用ParallelEntityModifier
对象的过渡效果。这些对象应该是全局的:/* These three Entity objects will represent different screens */ private final Entity mScreenOne = new Entity(); private final Entity mScreenTwo = new Entity(); private final Entity mScreenThree = new Entity(); /* This entity modifier is defined as the 'transition-in' modifier * which will move an Entity/screen into the camera-view */ private final ParallelEntityModifier mMoveInModifier = new ParallelEntityModifier( new MoveXModifier(3, WIDTH, 0), new RotationModifier(3, 0, 360), new ScaleModifier(3, 0, 1)); /* This entity modifier is defined as the 'transition-out' modifier * which will move an Entity/screen out of the camera-view */ private final ParallelEntityModifier mMoveOutModifier = new ParallelEntityModifier( new MoveXModifier(3, 0, -WIDTH), new RotationModifier(3, 360, 0), new ScaleModifier(3, 1, 0));
-
创建
mScene
对象,重写其onManagedUpdate()
方法以便处理调用下一步引入的setLayer(pLayerIn, pLayerOut)
方法。此外,我们将在创建mScene
对象后附加我们的Entity
对象层次:mScene = new Scene() { /* Variable which will accumulate time passed to * determine when to switch screens */ float timeCounter = 0; /* Define the first screen indices to be transitioned in and out */ int layerInIndex = 0; int layerOutIndex = SCREEN_COUNT - 1; /* Execute the code below on every update to the mScene object */ @Override protected void onManagedUpdate(float pSecondsElapsed) { /* If accumulated time is equal to or greater than 4 seconds */ if (timeCounter >= 4) { /* Set screens to be transitioned in and out */ setLayer(mScene.getChildByIndex(layerInIndex), mScene.getChildByIndex(layerOutIndex)); /* Reset the time counter */ timeCounter = 0; /* Setup the next screens to be swapped in and out */ if (layerInIndex >= SCREEN_COUNT - 1) { layerInIndex = 0; layerOutIndex = SCREEN_COUNT - 1; } else { layerInIndex++; layerOutIndex = layerInIndex - 1; } } /* Accumulate seconds passed since last update */ timeCounter += pSecondsElapsed; super.onManagedUpdate(pSecondsElapsed); } }; /* Attach the layers to the scene. * Their layer index (according to mScene) is relevant to the * order in which they are attached */ mScene.attachChild(mScreenOne); // layer index == 0 mScene.attachChild(mScreenTwo); // layer index == 1 mScene.attachChild(mScreenThree); // layer index == 2
-
最后,我们将创建一个
setLayer(pLayerIn, pLayerOut)
方法,我们可以用它来处理将实体修改器注册到适当的Entity
对象,根据它是否应该进入或离开相机视角:/* This method is used to swap screens in and out of the camera-view */ private void setLayer(IEntity pLayerIn, IEntity pLayerOut) { /* If the layer being transitioned into the camera-view is invisible, * set it to visibile */ if (!pLayerIn.isVisible()) { pLayerIn.setVisible(true); } /* Global modifiers must be reset after each use */ mMoveInModifier.reset(); mMoveOutModifier.reset(); /* Register the transitional effects to the screens */ pLayerIn.registerEntityModifier(mMoveInModifier); pLayerOut.registerEntityModifier(mMoveOutModifier); }
它是如何工作的…
这个食谱涵盖了与Entity
层次转换相关的一个简单但有用的系统。更大的游戏可能会涉及更多变量来考虑层次交换,但这个概念对于所有项目规模中的实体/屏幕索引和创建屏幕转换方法都是相关的。
在第一步中,我们将创建全局对象。三个Entity
对象将代表游戏内的不同屏幕。在此食谱中,三个Entity
对象都包含四个Rectangle
子对象,这使我们能够可视化屏幕过渡,然而我们可以将每个Entity
对象解释为不同的屏幕,如菜单屏幕、加载屏幕和游戏玩法屏幕。我们还创建了两个全局ParallelEntityModifier
实体修改器,以处理屏幕的位置变化。mMoveInModifier
修改器将把注册的屏幕从相机视角右侧外部移动到相机视角中心。mMoveOutModifier
修改器将把注册的屏幕从相机视角中心移动到相机视角左侧外部。这两个修改器都包括一个简单的旋转和缩放效果,以产生“滚动”过渡效果。
在下一步中,我们将创建mScene
对象并将全局声明的Entity
对象附加到它上面。在这个食谱中,我们设置mScene
对象根据经过的时间处理屏幕交换,然而在讨论mScene
对象的onManagedUpdate()
方法如何处理屏幕交换之前,让我们看看如何获取Entity
对象的索引,因为它们将用于确定哪些屏幕将被转换:
mScene.attachChild(mScreenOne); // layer index == 0
mScene.attachChild(mScreenTwo); // layer index == 1
mScene.attachChild(mScreenThree); // layer index == 2
如这段代码所示,我们根据名称以数字顺序附加屏幕。一旦Entity
对象被附加到Scene
对象,我们就可以在父对象上调用getChildByIndex(pIndex)
方法,以通过其索引获取Entity
对象。子项的索引由它们附加到另一个对象的顺序决定。我们在mScene
对象的onManagedUpdate()
方法中使用这些索引,以确定每四秒需要交换到摄像机视野中以及需要从视野中移出的实体/屏幕。
在初始化mScene
对象期间,我们实例化了两个int
变量,用于确定哪些屏幕需要进出摄像机的视野。最初,我们将layerInIndex
定义为值0
,这等于mScreenOne
对象的索引,并将layerOutIndex
定义为值SCREEN_COUNT – 1
,这等于按附加到Scene
对象的顺序mScreenThree
对象的索引。在mScene
对象的onManagedUpdate()
方法中每四秒,我们会调用setLayer(pLayerIn, pLayerOut)
方法来开始屏幕过渡,将timeCounter
变量重置为累积下一个四秒,并确定下一个需要进出摄像机视野的Entity
对象。虽然这个例子并不完全适用于大多数游戏,但它旨在让我们了解如何使用子索引来通过setLayer(pLayerIn,pLayerOut)
方法进行过渡调用。
在最后一步中,我们引入了setLayer(pLayerIn, pLayerOut)
方法,它处理将实体修改器应用于通过参数传递的Entity
对象。这个方法有三个目标;首先,如果当前不可见,它将设置正在过渡到视图中的层为可见,它重置我们的mMoveInModifier
和mMoveOutModifier
对象,以便它们可以为Entity
对象提供完整的过渡效果,最后,它在pLayerIn
和pLayerOut
参数上调用registerEntityModifier(pEntityModifier)
,在Entity
对象上启动过渡效果。
还有更多...
这个方法仅适用于在游戏中使用多个Entity
对象作为不同屏幕的游戏结构。然而,如何在屏幕之间处理过渡完全取决于开发者。在做出决定之前,了解我们处理游戏中多个屏幕的不同选择的好坏是明智的。请查看以下列表,了解不同方法的优缺点:
-
活动/屏幕:
-
优点:通过简单调用活动的
finish()
方法,Android 操作系统将为我们处理资源卸载,使得资源管理变得非常简单。 -
缺点:每个屏幕过渡都会在启动新活动/屏幕时显示短暂的黑色屏幕。
-
缺点:必须为每个活动加载各自的资源。这意味着预加载资源不是一个选项,这可能会增加整体加载时间,尤其是考虑到可能在所有屏幕上使用的资源,如字体或音乐播放资源。
-
缺点:由于 Android 的内存管理功能,被视为后台进程的活动可能会在任何时候被杀死,假设设备内存不足。这会在我们离开一个应该保持暂停状态直到用户返回的活动时造成问题。有可能当我们需要时,从任何活动转换而来的状态可能无法以相同的状态返回。
-
-
场景/屏幕:
-
优点:有可能预加载可能跨多个屏幕使用的必要资源。这可以大大帮助减少加载时间,具体取决于可预加载资源的数量。
-
优点:我们能够在游戏中引入加载屏幕,而不是在资源加载时显示空白屏幕。
-
优点/缺点:必须开发一个屏幕和资源管理系统,以便处理资源的加载/卸载和屏幕的切换。根据特定游戏的大小和需求,这可能是一个相当大的任务。然而,这种方法可以在屏幕间移动时实现无缝过渡,因为我们可以更方便地加载/卸载资源,而不是在用户决定切换屏幕时立即进行。
-
缺点:通常一次只能将一个
Scene
对象应用到我们的 Engine 对象上,这意味着屏幕过渡在动画/流畅性方面将会有所不足。设置的屏幕将简单地替换之前的屏幕。
-
-
实体/屏幕:
-
优点:当处理
Entity
对象作为屏幕时,我们可以将任意数量的对象附加到一个Scene
对象。这使我们能够获得场景/屏幕方法的所有优点,同时增加了能够添加基于时间的过渡效果的好处,例如从菜单屏幕“滑动”到加载屏幕,再到游戏屏幕。这正是本教程代码所展示的。 -
优点/缺点:与场景/屏幕方法一样,我们需要自己处理所有屏幕和资源的清理。优点大于缺点,但是与活动/屏幕方法相比,根据项目的大小,某些人可能会认为需要屏幕/资源管理系统是一个缺点。
-
在结束这个教程之前,还有一个重要的话题在本教程中没有讨论。请看下面的图,它展示了这个教程在设备上的显示结果可能的样子:
前图展示了用户在游戏内不同屏幕间导航时典型的过渡事件。我们讲解过这种导航是如何通过将新屏幕带入摄像机视野来实现的。更重要的是,这些过渡事件还应该处理资源的加载和卸载。毕竟,没有理由在菜单屏幕未展示给用户时还让它占用设备宝贵的资源。在理想情况下,如果我们如前图所示从菜单屏幕移动到游戏玩法屏幕,在T1阶段,游戏玩法屏幕将开始加载其资源。一旦达到T2阶段,意味着加载屏幕成为游戏当前的主屏幕,此时会卸载菜单屏幕的所有必要资源,并将其从Scene
对象中分离,以移除不必要的开销。
这只是关于如何在游戏中处理屏幕间过渡以实现流畅过渡和减少过渡间的加载时间的一个简要概述。关于屏幕管理的内部工作原理的更深入信息,请参见第五章《场景和图层管理》。
另请参阅
-
在第二章《使用实体》中的了解 AndEngine 实体。
-
在第二章《使用实体》中覆盖 onManagedUpdate()方法。
-
在第二章《使用实体》中的使用修饰符和实体修饰符。
第四章:使用摄像机
本章将介绍 AndEngine 的各种摄像机对象和高级摄像机控制。主题包括:
-
引入摄像机对象
-
使用边界摄像机限制摄像机区域
-
使用缩放摄像机更近距离观察
-
使用平滑摄像机创建平滑移动
-
捏合缩放摄像机功能
-
拼接背景
-
为摄像机应用 HUD
-
将控制器附加到显示
-
坐标转换
-
创建分屏游戏
引言
AndEngine 包括三种类型的摄像机,不包括基础的Camera
对象,这允许我们更具体地控制摄像机的行为。摄像机在游戏中可以扮演许多不同的角色,在某些情况下,我们可能需要不止一个摄像机。这一章将介绍我们可以使用 AndEngine 的Camera
对象的不同目的和方式,以便在我们的游戏中应用更高级的摄像机功能。
引入摄像机对象
在设计大型游戏时,摄像机可以有许多用途,但它的主要目标是将在游戏世界的特定区域显示在设备的屏幕上。这一主题将介绍基础的Camera
类,涵盖摄像机的一般方面,以便为将来的摄像机使用提供参考。
如何操作...
在游戏开发中,摄像机的重要性在于它决定了我们在设备上能看到什么。创建我们的摄像机就像下面的代码一样简单:
final int WIDTH = 800;
final int HEIGHT = 480;
// Create the camera
Camera mCamera = new Camera(0, 0, WIDTH, HEIGHT);
WIDTH
和HEIGHT
值将定义游戏场景的区域,该区域将在设备上显示。
它是如何工作的...
重要的是要了解摄像机的主要功能,以便在我们的项目中充分利用它。所有不同的摄像机都继承了本主题中找到的方法。让我们看看在 AndEngine 开发中一些最必要的摄像机方法:
摄像机定位:
Camera
对象遵循与实体相同的坐标系。例如,将摄像机的坐标设置为(0,0)
,将设置摄像机的中心点为定义的坐标。此外,增加 x 值将摄像机向右移动,增加 y 值将摄像机向上移动。减少这些值将产生相反的效果。为了将摄像机重新定位到定义的位置中心,我们可以调用以下方法:
// We can position the camera anywhere in the game world
mCamera.setCenter(WIDTH / 2, HEIGHT / 2);
上述代码对默认的摄像机位置没有任何影响(假设WIDTH
和HEIGHT
值用于定义摄像机的宽度和高度)。这将设置摄像机的中心到我们场景的“中心”,当创建Camera
对象时,这自然等于摄像机WIDTH
和HEIGHT
值的一半。在需要将摄像机重置回初始位置的情况下,可以使用前面的方法调用,这在摄像机在游戏过程中移动,但在用户返回菜单时应返回初始位置时很有用。
不设置特定坐标而移动摄像头可以通过offsetCenter(x,y)
方法实现,其中x
和y
值定义了在场景坐标中偏移摄像头的距离。此方法将指定的参数值添加到摄像头的当前位置:
// Move the camera up and to the right by 5 pixels
mCamera.offsetCenter(5, 5);
// Move the camera down and to the left by 5 pixels
mCamera.offsetCenter(-5, -5);
此外,我们可以通过以下方法获取摄像头的中心坐标(x 和 y):
mCamera.getCenterX();
mCamera.getCenterY();
调整摄像头的宽度和高度:
可以通过摄像头的set()
方法调整摄像头的初始宽度和高度。我们还可以通过调用如setXMin()
/setXMax()
和setYMin()
/setYMax()
等方法来设置摄像头的最小/最大 x 和 y 值。以下代码将使摄像头宽度减半,同时保持初始的摄像头高度:
// Shrink the camera by half its width
mCamera.set(0, 0, mCamera.getWidth() / 2, mCamera.getHeight());
需要注意的是,在缩小摄像头宽度的同时,我们会失去在定义区域之外的像素和任何实体的可见性。此外,缩小或扩展摄像头的宽度和高度可能会导致实体看起来被拉伸或压缩。通常,在开发典型游戏时,修改摄像头的宽度和高度并不是必要的。
Camera
对象还允许我们通过调用getXMin()
/getXMax()
和getYMin()
/getYMax()
获取摄像头的当前最小/最大宽度和高度值。
可见性检查:
Camera
类允许我们检查特定的Entity
对象是否在摄像头的视野内可见。Entity
对象子类型包括但不限于Line
和Rectangle
基元,Sprite
和Text
对象,以及它们的子类型,如TiledSprite
和ButtonSprite
对象等。可以通过以下方法进行可见性检查:
// Check if entity is visible. true if so, false otherwise
mCamera.isEntityVisible(entityObject);
可见性检查对于许多游戏来说非常有用,例如,重用可能离开摄像头视野的对象,这样就可以限制在可能产生大量对象并最终离开摄像头视野的情况下创建对象的总数。相反,我们可以重用离开摄像头视野的对象。
追逐实体功能:
在很多游戏中,常常需要摄像头跟随屏幕上的Entity
对象移动,例如在横向卷轴游戏中。我们可以通过调用一个简单的方法轻松设置摄像头跟随游戏世界中任何地方的实体移动。
mCamera.setChaseEntity(entityObject);
之前的代码将在每次更新摄像头时将摄像头位置应用到指定实体的位置上。这确保了实体始终处于摄像头的中心。
注意:由于原文最后一行只有一个单词"Note",并没有提供足够的信息来进行翻译,因此在这里保留原文。如果需要进一步的翻译,请提供完整的句子或段落。
在本书的多数食谱中,我们指定了 800 像素的摄像头宽度和 480 像素的摄像头高度。然而,这些值完全取决于开发者,并且应由游戏的需求来定义。选择这些特定的值是因为它们相对适合小屏幕和大屏幕设备。
使用边界摄像头限制摄像头区域
BoundCamera
对象允许我们定义摄像机区域的具体边界,限制摄像机在 x 轴和 y 轴上可以移动的距离。当摄像机需要跟随玩家但又不超出关卡边界时(例如用户靠近墙壁时),这种摄像机非常有用。
如何操作...
创建BoundCamera
对象需要与普通Camera
对象相同的参数:
BoundCamera mCamera = new BoundCamera(0, 0, WIDTH, HEIGHT);
它是如何工作的...
BoundCamera
对象扩展了普通的Camera
对象,为我们提供了本章中摄像机对象介绍一节描述的所有原始摄像机功能。实际上,除非我们在BoundCamera
对象上配置了一个有边界的区域,否则我们实际上是在使用基本的Camera
对象。
在摄像机对其可移动区域应用限制之前,我们必须定义摄像机可以自由移动的可用区域:
// WIDTH = 800;
// HEIGHT = 480;
// WIDTH and HEIGHT are equal to the camera's width and height
mCamera.setBounds(0, 0, WIDTH * 4, HEIGHT);
// We must call this method in order to apply camera bounds
mCamera.setBoundsEnabled(true);
上述代码将从场景坐标(0,0)
的位置开始设置摄像机边界,一直到(3200,480)
,因为我们把摄像机的宽度放大了四倍作为最大 x 区域,允许摄像机滚动四倍于其宽度。由于边界高度设置为与摄像机高度相同的值,摄像机将不会响应 y 轴上的变化。
另请参阅
- 本章节提供的摄像机对象介绍。
用缩放摄像机更近距离地观察
AndEngine 的BoundCamera
和Camera
对象默认不支持放大和缩小。如果我们想要允许摄像机缩放,可以创建一个扩展了BoundCamera
类的ZoomCamera
对象。这个对象包括其继承类所有的功能,包括创建摄像机边界。
如何操作...
ZoomCamera
对象与BoundCamera
类似,在创建摄像机时不需要定义额外的参数:
ZoomCamera mCamera = new ZoomCamera(0, 0, WIDTH, HEIGHT);
它是如何工作的…
为了向摄像机应用缩放效果,我们可以调用setZoomFactor(factor)
方法,其中factor
是我们想要应用到Scene
对象的放大倍数。通过以下代码可以实现放大和缩小:
// Divide the camera width/height by 1.5x (Zoom in)
mCamera.setZoomFactor(1.5f);
// Divide the camera width and height by 0.5x (Zoom out)
mCamera.setZoomFactor(0.5f);
在处理摄像机的缩放因子时,我们必须知道1
的因子等于Camera
类的默认因子。大于1
的缩放因子将摄像机向场景内缩放,而任何小于1
的值将使摄像机向外缩放。
处理缩放因子的数学运算非常基础。摄像机只需将缩放因子除以我们摄像机的WIDTH
和HEIGHT
值,有效实现摄像机的“缩放”。如果我们的摄像机宽度是800
,那么1.5f
的缩放因子将使摄像机向内缩放,最终将摄像机的宽度设置为533.3333
,这将限制场景显示的区域面积。
注意
在应用了缩放因子(不等于 1)的情况下,ZoomCamera
对象返回的getMinX()
、getMaxX()
、getMinY()
、getMaxY()
、getWidth()
和getHeight()
值会自动被缩放因子除。
还有更多…
在缩放摄像头中启用不等于 1 的因子的边界,将对摄像头能够平移的总可用区域产生影响。假设边界的最小和最大 x 值从 0 设置为 800,如果摄像头宽度等于 800,那么在 x 轴上将不允许有任何移动。如果我们放大摄像头,摄像头的宽度将减小,从而允许摄像头移动时有更多的余地。
注意
如果定义了一个缩放因子,导致摄像头的宽度或高度超出摄像头边界,那么将应用缩放因子到摄像头,但超出轴将不允许移动。
另请参阅
-
本章中提供的摄像头对象介绍。
-
本章中提供的限制摄像头区域的边界摄像头。
使用平滑摄像头创建平滑移动
SmoothCamera
对象是四种可选摄像头中最先进的一个。这个摄像头支持所有不同的摄像头功能类型(边界、缩放等),并新增了一个选项,即在为摄像头设置新位置时,可以给摄像头的移动速度应用一个定义好的速度。这样做的结果是,摄像头在移动时看起来会“平滑”地进入和退出,从而实现相当微妙的摄像头移动。
如何操作…
这种摄像头类型是四种中唯一需要在构造函数中定义额外参数的一个。这些额外的参数包括摄像头可以移动的最大 x 和 y 速度以及处理摄像头缩放速度的最大缩放因子变化。让我们看看创建这种摄像头的样子:
// Camera movement speeds
final float maxVelocityX = 10;
final float maxVelocityY = 5;
// Camera zoom speed
final float maxZoomFactorChange = 5;
// Create smooth camera
mCamera = new SmoothCamera(0, 0, WIDTH, HEIGHT, maxVelocityX, maxVelocityY, maxZoomFactorChange);
工作原理…
在这个示例中,我们将创建一个摄像头,为摄像头的移动和缩放应用平滑的过渡效果。与其他三种摄像头类型不同,不是直接使用setCenter(x,y)
将摄像头中心设置到定义的位置,而是使用maxVelocityX
、maxVelocityY
和maxZoomFactorChange
变量来定义摄像头从点 A 到点 B 的移动速度。增加速度会使摄像头移动更快。
对于SmoothCamera
类,无论是摄像头移动还是缩放,都有两种选择。我们可以通过调用这些任务的默认摄像头方法(camera.setCenter()
和camera.setZoomFactor()
)使摄像头平滑移动或缩放。另一方面,有时我们需要立即重新定位摄像头。这可以通过分别调用camera.setCenterDirect()
和camera.setZoomFactorDirect()
方法来实现。这些方法通常用于重置平滑摄像头的位置。
另请参阅
-
本章节中提供的相机对象介绍。
-
本章节中提到的限制相机区域的边界相机。
-
本章节中提供的通过缩放相机近距离观察。
捏合缩放相机功能
AndEngine 包含一系列“检测器”类,可以与场景触摸事件结合使用。本主题将介绍如何使用PinchZoomDetector
类,以便通过在屏幕上按两指,并让它们靠近或分开来调整缩放因子,从而允许相机的缩放。
开始操作…
请参考代码包中名为ApplyingPinchToZoom
的类。
如何操作…
按照以下步骤进行操作,以设置捏合缩放功能。
-
我们首先要做的是将适当的监听器实现到我们的类中。由于我们将处理触摸事件,因此需要包含
IOnSceneTouchListener
接口。此外,我们还需要实现IPinchZoomDetectorListener
接口,以处理相机缩放因子在等待触摸事件时的变化:public class ApplyingPinchToZoom extends BaseGameActivity implements IOnSceneTouchListener, IPinchZoomDetectorListener {
-
在
BaseGameActivity
类的onCreateScene()
方法中,将Scene
对象的触摸监听器设置为this
活动,因为我们让BaseGameActivity
类实现触摸监听器类。我们还将在此方法中创建并启用mPinchZoomDetector
对象:/* Set the scene to listen for touch events using * this activity's listener */ mScene.setOnSceneTouchListener(this); /* Create and set the zoom detector to listen for * touch events using this activity's listener */ mPinchZoomDetector = new PinchZoomDetector(this); // Enable the zoom detector mPinchZoomDetector.setEnabled(true);
-
在
BaseGameActivity
类的实现的onSceneTouchEvent()
方法中,我们必须将触摸事件传递给mPinchZoomDetector
对象:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { // Pass scene touch events to the pinch zoom detector mPinchZoomDetector.onTouchEvent(pSceneTouchEvent); return true; }
-
接下来,当
mPinchZoomDetector
对象检测到用户在屏幕上使用两指操作时,我们将获取ZoomCamera
对象的初始缩放因子。我们将使用通过IPinchZoomDetectorListener
接口实现的onPinchZoomStarted()
方法:/* This method is fired when two fingers press down * on the display */ @Override public void onPinchZoomStarted(PinchZoomDetector pPinchZoomDetector, TouchEvent pSceneTouchEvent) { // On first detection of pinch zooming, obtain the initial zoom factor mInitialTouchZoomFactor = mCamera.getZoomFactor(); }
-
最后,在检测到屏幕上出现捏合动作时,我们将更改
ZoomCamera
对象的缩放因子。这段代码将放在onPinchZoom()
和onPinchZoomFinished()
方法中:@Override public void onPinchZoom(PinchZoomDetector pPinchZoomDetector, TouchEvent pTouchEvent, float pZoomFactor) { /* On every sub-sequent touch event (after the initial touch) we offset * the initial camera zoom factor by the zoom factor calculated by * pinch-zooming */ final float newZoomFactor = mInitialTouchZoomFactor * pZoomFactor; // If the camera is within zooming bounds if(newZoomFactor < MAX_ZOOM_FACTOR && newZoomFactor > MIN_ZOOM_FACTOR){ // Set the new zoom factor mCamera.setZoomFactor(newZoomFactor); } }
工作原理…
在此食谱中,我们覆盖了发生在我们场景上的场景触摸事件,将这些触摸事件传递给PinchZoomDetector
对象,该对象将处理ZoomCamera
对象的缩放功能。以下步骤将引导我们了解捏合缩放的工作原理。由于在此活动中我们将使用缩放因子,因此我们需要使用ZoomCamera
类或SmoothCamera
类的实现。
在这个配方的第一步和第二步中,我们正在实现所需的监听器,并将它们注册到mScene
对象和mPinchZoomDetector
对象。由于ApplyingPinchToZoom
活动正在实现监听器,我们可以将代表我们BaseGameActivity
类的this
传递给mScene
对象作为触摸监听器。我们还可以将此活动作为捏合检测监听器。一旦创建了捏合检测器,我们可以通过调用setEnabled(pSetEnabled)
方法来启用或禁用它。
在第三步中,我们将pSceneTouchEvent
对象传递给捏合检测器的onTouchEvent()
方法。这样做可以让捏合检测器获取特定的触摸坐标,这些坐标将在内部用于根据手指位置计算缩放因子。
当在屏幕上按下两个手指时,捏合检测器将触发第四步中显示的代码片段。我们必须在此时获取相机的初始缩放因子,以便在触摸坐标改变时正确偏移缩放因子。
最后一步涉及计算偏移缩放因子并将其应用于ZoomCamera
对象。通过将初始缩放因子与PinchZoomDetector
对象计算的缩放因子变化相乘,我们可以成功偏移相机的缩放因子。一旦我们计算了newZoomFactor
对象的值,我们调用setZoomFactor(newZoomFactor)
以改变我们相机的缩放级别。
将缩放因子限制在特定范围内只需添加一个if
语句,指定我们需要的最大和/或最小缩放因子即可。在这种情况下,我们的相机不能缩放比0.5f
更小,或者比1.5f
更大。
另请参阅
- 本章提供了使用缩放相机近距离观察的内容。
拼接背景
尽管 AndEngine 的Scene
对象允许我们为场景设置背景,但这并不总是我们项目的可行解决方案。为了使背景能够进行平移和缩放,我们可以把多个纹理区域拼接在一起,并将其直接应用到场景中作为精灵。这一主题将要讲述如何将两个 800 x 480 的纹理区域拼接在一起,以创建一个更大的可平移和可缩放的背景。背景拼接背后的想法是允许场景的部分以较小的块显示。这为我们提供了创建较小纹理尺寸的机会,以避免超过大多数设备 1024 x 1024 的最大纹理尺寸限制。此外,我们可以启用剪裁,以便在屏幕上不显示场景部分时,不绘制它们,以提高性能。以下图展示了结果:
开始使用...
执行本章给出的食谱捏合缩放相机功能,以了解捏合缩放的工作原理。此外,我们还需要准备两张单独的 800 x 480 像素的图片,类似于本食谱引言中的前一个图像,以 PNG 格式保存,然后在代码包中引用名为StitchedBackground
的类。
如何操作…
背景拼接是一个简单的概念,它涉及将两个或更多的精灵直接并排放置,重叠放置,或者上下放置,以形成看似拥有一个单一的、大精灵的效果。在本食谱中,我们将介绍如何做到这一点,以避免可怕的纹理溢出效应。按照以下步骤操作:
-
首先,我们需要创建我们的
BuildableBitmapTextureAtlas
和ITextureRegion
对象。非常重要的一点是,纹理图集的大小必须与我们的图片文件完全相同,以避免纹理溢出。同时,在构建纹理图集的过程中,我们绝不能包含任何填充或间隔。以下代码将创建左侧的纹理图集和纹理区域,同样的代码也适用于右侧:/* Create the background left texture atlas */ BuildableBitmapTextureAtlas backgroundTextureLeft = new BuildableBitmapTextureAtlas( mEngine.getTextureManager(), 800, 480); /* Create the background left texture region */ mBackgroundLeftTextureRegion = BitmapTextureAtlasTextureRegionFactory .createFromAsset(backgroundTextureLeft, getAssets(), "background_left.png"); /* Build and load the background left texture atlas */ try { backgroundTextureLeft .build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>( 0, 0, 0)); backgroundTextureLeft.load(); } catch (TextureAtlasBuilderException e) { e.printStackTrace(); }
-
一旦纹理资源就位,我们就可以移动到活动的
onPopulateScene()
方法中,在那里我们将创建并将精灵应用到Scene
对象上:final int halfTextureWidth = (int) (mBackgroundLeftTextureRegion.getWidth() * 0.5f); final int halfTextureHeight = (int) (mBackgroundLeftTextureRegion.getHeight() * 0.5f); // Create left background sprite mBackgroundLeftSprite = new Sprite(halfTextureWidth, halfTextureHeight, mBackgroundLeftTextureRegion, mEngine.getVertexBufferObjectManager()) ; // Attach left background sprite to the background scene mScene.attachChild(mBackgroundLeftSprite); // Create the right background sprite, positioned directly to the right of the first segment mBackgroundRightSprite = new Sprite(mBackgroundLeftSprite.getX() + mBackgroundLeftTextureRegion.getWidth(), halfTextureHeight, mBackgroundRightTextureRegion, mEngine.getVertexBufferObjectManager()); // Attach right background sprite to the background scene mScene.attachChild(mBackgroundRightSprite);
它是如何工作的…
背景拼接可以在许多不同的场景中使用,以避免某些问题。这些问题包括导致某些设备不兼容的过大纹理尺寸,不响应相机位置或缩放因子变化的静态背景,以及性能问题等。在本食谱中,我们创建了一个大背景,这是通过将两个Sprite
对象并排放置拼接而成的,每个代表不同的TextureRegion
对象。结果是形成一个大于相机宽度两倍的大背景,尺寸为 1600 x 480 像素。
在处理允许场景滚动的拼接背景的大多数情况下,我们将需要启用一些相机边界,以防止在相机试图超出背景区域时更新相机位置。我们可以使用ZoomCamera
对象来实现这一点,将边界设置为背景预定的尺寸。由于我们处理的是两个各为 800 x 480 像素的 PNG 图片并排拼接,可以肯定地说,坐标(0,0)
到(1600, 480)
足以作为相机边界。
如第一步所述,使用这种方法创建大型背景时,我们必须遵循一些规则。图像大小必须与BuildableBitmapTextureAtlas
纹理图集大小完全相同!不遵循此规则可能会导致精灵之间周期性地出现伪影,这对玩家来说是非常分散注意力的。这也意味着我们不应该在用于背景拼接的BuildableBitmapTextureAtlas
对象中包含超过一个ITextureRegion
对象。在这种情况下,我们还应该避免使用填充和间距功能。然而,遵循这些规则,我们仍然可以对纹理图集应用TextureOptions.BILINEAR
纹理过滤,并且不会导致问题。
在第二步中,我们继续创建Sprite
对象。这里没有特别之处;我们只是在给定位置创建一个Sprite
对象,然后在第一个旁边直接设置下一个精灵。对于极其庞大和多样的背景,将纹理拼接在一起的方法可以帮助显著降低应用程序的性能成本,允许我们停止渲染不再可见的背景较小部分。这个特性称为剔除。有关如何实现这一点,请参见第八章,最大化性能中的通过实体剔除禁用渲染。
参见 also(此处的"also"似乎是原文的残留,若不需要翻译请忽略)
-
在第二章,设计您的菜单中,介绍使用精灵让场景生动。
-
本章节提供通过缩放相机更近距离观察。
-
本章节介绍捏合缩放相机功能。
-
在第八章,最大化性能中,介绍通过实体剔除禁用渲染。
向相机应用 HUD。
即使是最简单的游戏,HUD(抬头显示)也可能是一个非常实用的组件。HUD 的目的是包含一组按钮、文本或任何其他Entity
对象,以便为用户提供界面。HUD 有两个关键点:第一,无论相机是否改变位置,HUD 的子对象始终会在屏幕上显示;第二,HUD 的子对象始终会显示在场景子对象的前面。在本章中,我们将向相机应用 HUD,以便在游戏过程中为用户提供界面。
如何操作...
将以下代码导入您选择的任何BaseGameActivity
的onCreateEngineOptions()
方法中,如果需要,请替换此代码片段中的相机类型:
@Override
public EngineOptions onCreateEngineOptions() {
// Create the camera
Camera mCamera = new Camera(0, 0, WIDTH, HEIGHT);
// Create the HUD
HUD mHud = new HUD();
// Attach the HUD to the camera
mCamera.setHUD(mHud);
EngineOptions engineOptions = new EngineOptions(true,
ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(),
mCamera);
return engineOptions;
}
它是如何工作的…
使用HUD
类通常是一项非常简单的任务。根据所创建的游戏类型,HUD
类的实用性可能会有很大差异,但无论如何,在决定使用这个类之前,我们必须了解一些事情:
-
HUD
实体在相机移动时不会改变位置。一旦定义了它们的位置,实体将保持在该屏幕位置,除非通过setPosition()
方法进行设置。 -
HUD
实体将始终出现在任何Scene
实体的顶部,无论 z-index、应用顺序或任何其他场景如何。 -
在任何情况下都不应将剔除应用于要附加到
HUD
类的实体。剔除以相同的方式影响HUD
类上的Entity
对象,就像它会影响Scene
对象上的Entity
对象一样,即使Entity
对象似乎没有移出屏幕。这将导致看似随机消失的HUD
实体。只是不要这么做!
在如何操作...部分的代码中,我们可以看到设置HUD
类非常简单。创建并应用HUD
对象到相机只需以下两行代码即可完成:
// Create the HUD
HUD mHud = new HUD();
// Attach the HUD to the camera
mCamera.setHUD(mHud);
从这一点开始,我们可以将HUD
对象视为游戏中任何其他层的实体应用。
将控制器应用于显示
根据我们正在创建的游戏类型,玩家互动有许多可能的解决方案。AndEngine 包含两个独立的类,其中一个模拟方向控制板,称为DigitalOnScreenControl
,另一个模拟摇杆,称为AnalogOnScreenControl
。本主题将介绍 AndEngine 的AnalogOnScreenControl
类,但使用这个类将给我们足够的信息去使用任一控制器。
开始吧...
此配方需要两个独立的资源,它们将作为控制器的基础和旋钮。在继续如何操作...部分之前,请将名为controller_base.png
和controller_knob.png
的图片包含到您选择的项目中的assets/gfx
文件夹中。这些图片可能看起来像下面的图,基础为 128 x 128 像素,旋钮为 64 x 64 像素:
如何操作...
一旦我们为控制器准备好了两个必要的资源,我们就可以开始编码了。首先,我们可以开始创建保存控制器资源的ITextureRegion
和BuildableBitmapTextureAtlas
对象。对于控制器纹理图集或纹理区域没有特殊步骤;像创建普通精灵一样创建它们。像往常一样,在您选择的活动中的onCreateResources()
方法中完成此操作。
一旦ITextureRegion
对象被编码并准备好在活动内使用,我们可以在活动对象的onCreateScene()
方法中创建AnalogOnScreenControl
类,如下所示:
// Position the controller in the bottom left corner of the screen
final float controllerX = mControllerBaseTextureRegion.getWidth();
final float controllerY = mControllerBaseTextureRegion.getHeight();
// Create the controller
mController = new AnalogOnScreenControl(controllerX, controllerY, mCamera, mControllerBaseTextureRegion, mControllerKnobTextureRegion, 0.1f, mEngine.getVertexBufferObjectManager(), new IAnalogOnScreenControlListener(){
/* The following method is called every X amount of seconds,
* where the seconds are determined by the pTimeBetweenUpdates
* parameter in the controller's constructor */
@Override
public void onControlChange(
BaseOnScreenControl pBaseOnScreenControl, float pValueX,
float pValueY) {
mCamera.setCenter(mCamera.getCenterX() + (pValueX * 10), mCamera.getCenterY() + (pValueY * 10));
Log.d("Camera", String.valueOf(mCamera.getCenterX()));
}
// Fired when the knob is simply pressed
@Override
public void onControlClick(
AnalogOnScreenControl pAnalogOnScreenControl) {
// Do nothing
}
});
// Initialize the knob to its center position
mController.refreshControlKnobPosition();
// Set the controller as a child scene
mScene.setChildScene(mController);
工作原理...
如我们所见,一些参数与我们创建Sprite
对象时定义的参数并无不同。前五个参数是自解释的。第六个参数(0.1f)
是“更新之间的时间”参数。这个值控制onControlChange()
方法内的事件被触发的频率。对于 CPU 密集型代码,增加更新之间的时间可能有益,而对于复杂性较低的代码,非常低的更新时间可能没有问题。
控制器构造函数中必须包含的最后一个参数是IanalogOnScreenControlListener
,它处理基于控制器是被简单点击还是被按住并保持在一个偏移位置的事件。
正如我们在onControlChange()
事件中所见,我们可以通过pValueX
和pValueY
变量获取控制器旋钮的当前位置。这些值包含了控制器的 x 和 y 偏移量。在本示例中,我们使用旋钮的 x 和 y 偏移量来移动摄像头的位置,这也让我们了解到如何使用这些变量来移动其他实体,例如玩家的精灵。
坐标转换
在某些场景对象依赖于多个实体作为游戏精灵的基础层的场景中,坐标转换可能非常有用。在包含许多父对象,每个父对象都有自己的子对象集合的游戏中,需要获取子对象相对于Scene
对象的位置是常有的事。在所有层在整个游戏中始终保持相同的(0, 0)坐标的情况下,这不是问题。另一方面,当我们的层开始移动时,子对象的位置会随父对象移动,但它们在层上的坐标保持不变。本主题将涵盖将场景坐标转换为局部坐标,以允许嵌套实体在场景上正确定位。
如何操作…
将以下代码导入你选择的任何BaseGameActivity
的onCreateScene()
方法中。
-
本方法的第一个步骤是创建一个
Rectangle
对象并将其应用到Scene
对象上。这个Rectangle
对象将作为另一个Rectangle
对象的父实体。我们将它的颜色设置为蓝色,以便当两个矩形重叠时可以区分,因为父Rectangle
对象将不断移动:/* Create a rectangle on the Scene that will act as a layer */ final Rectangle rectangleLayer = new Rectangle(0, HEIGHT * 0.5f, 200, 200, mEngine.getVertexBufferObjectManager()){ /* Obtain the half width of this rectangle */ int halfWidth = (int) (this.getWidth() * 0.5f); /* Boolean value to determine whether to pan left or right */ boolean incrementX = true; @Override protected void onManagedUpdate(float pSecondsElapsed) { float currentX = this.getX(); /* Determine whether or not the layer should pan left or right */ if(currentX + halfWidth > WIDTH){ incrementX = false; } else if (currentX - halfWidth < 0){ incrementX = true; } /* Increment or decrement the layer's position based on incrementX */ if(incrementX){ this.setX(currentX + 5f); } else { this.setX(currentX - 5f); } super.onManagedUpdate(pSecondsElapsed); } }; rectangleLayer.setColor(0, 0, 1); // Attach the layer to the scene mScene.attachChild(rectangleLayer);
-
接下来,我们将子
Rectangle
对象添加到我们先前创建的第一个Rectangle
对象中。这个Rectangle
对象不会移动;相反,它将保持在屏幕中心,而其父对象继续在周围移动。这个Rectangle
对象将利用坐标转换来保持其位置:/* Create a smaller, second rectangle and attach it to the first */ Rectangle rectangle = new Rectangle(0, 0, 50, 50, mEngine.getVertexBufferObjectManager()){ /* Obtain the coordinates in the middle of the Scene that we will * convert to everytime the parent rectangle moves */ final float convertToMidSceneX = WIDTH * 0.5f; final float convertToMidSceneY = HEIGHT * 0.5f; @Override protected void onManagedUpdate(float pSecondsElapsed) { /* Convert the specified x/y coordinates into Scene coordinates, * passing the resulting coordinates into the convertedCoordinates array */ final float convertedCoordinates[] = rectangleLayer.convertSceneCoordinatesToLocalCoordinates(convertToMidSceneX, convertToMidSceneY); /* Since the parent is moving constantly, we must adjust this rectangle's * position on every update as well. This will keep in in the center of the * display at all times */ this.setPosition(convertedCoordinates[0], convertedCoordinates[1]); super.onManagedUpdate(pSecondsElapsed); } }; /* Attach the second rectangle to the first rectangle */ rectangleLayer.attachChild(rectangle);
它是如何工作的…
上面的onCreateScene()
方法创建了一个包含两个独立Rectangle
实体的Scene
对象。第一个Rectangle
实体将直接附加到Scene
对象上。第二个Rectangle
实体将附加到第一个Rectangle
实体上。名为rectangleLayer
的第一个Rectangle
实体将会持续地从左向右和从右向左移动。通常,这会导致其子实体的位置跟随相同的移动模式,但在这个示例中,我们使用坐标转换,以允许子Rectangle
实体在其父实体移动时保持静止。
在此示例中,rectangle
对象包括两个名为convertToMidSceneX
和convertToMidSceneY
的变量。这些变量简单地保存了我们想要将局部坐标转换到的Scene
坐标中的位置。正如我们所看到的,它们的坐标被定义在场景的中间。在rectangle
对象的onManagedUpdate()
方法中,我们然后使用rectangleLayer.convertSceneCoordinatesToLocalCoordinates(convertToMidSceneX, convertToMidSceneY)
方法,将结果坐标传递给一个浮点数组。这样做的基本上是询问rectangleLayer
对象:“在你看来,场景上的位置 x/y 在哪里?”由于rectangleLayer
对象直接附加到Scene
对象,它可以轻松地确定特定Scene
坐标的位置,因为它依赖于原生的Scene
坐标系统。
当尝试访问返回的坐标时,我们可以通过convertedCoordinates[0]
获取转换后的 x 坐标,并使用convertedCoordinates[1]
获取转换后的 y 坐标。
在将Scene
坐标转换为局部Entity
坐标的基础上,我们还可以将局部Entity
坐标转换为Scene
坐标、触摸事件坐标、摄像头坐标以及许多其他选项。然而,一旦我们从这个示例开始,对坐标转换有了基本的了解,其余的转换方法将看起来非常相似。
创建一个分屏游戏
本示例将介绍DoubleSceneSplitScreenEngine
类,该类通常用于允许多个玩家在显示器的每一半上玩他们自己的游戏实例的游戏中。DoubleSceneSplitScreenEngine
类使我们能够为设备的显示器的每一半提供自己的Scene
和Camera
对象,从而让我们完全控制显示器每一半将看到的内容。
开始使用…
请参考代码包中名为SplitScreenExample
的类。
如何操作…
要使我们的游戏支持两个独立的Scene
对象,我们需要在最初设置BaseGameActivity
类时采取略有不同的方法。然而,一旦我们设置好了独立的Scene
对象,管理它们实际上与只处理一个场景非常相似,除了每个场景只有原始显示空间的一半这一点。执行以下步骤以了解如何设置DoubleSceneSplitScreenEngine
类。
-
我们首先需要将
WIDTH
值减半,因为每个相机将需要设备显示的一半空间。试图将 800 像素的宽度适配到每个相机将导致每个场景上的对象出现明显的扭曲。在声明变量时,我们还将设置两个Scene
对象和两个Camera
对象,这些将用于DoubleSceneSplitScreenEngine
的实现:public static final int WIDTH = 400; public static final int HEIGHT = 480; /* We'll need two Scene's for the DoubleSceneSplitScreenEngine */ private Scene mSceneOne; private Scene mSceneTwo; /* We'll also need two Camera's for the DoubleSceneSplitScreenEngine */ private SmoothCamera mCameraOne; private SmoothCamera mCameraTwo;
-
然后,我们将在
BaseGameActivity
类的onCreateEngineOptions()
方法中创建两个独立的SmoothCamera
对象。这些相机将用于为显示的每一半提供独立的视图。在这个示例中,我们应用了自动缩放,以展示DoubleSceneSplitScreenEngine
的结果:/* Create the first camera (Left half of the display) */ mCameraOne = new SmoothCamera(0, 0, WIDTH, HEIGHT, 0, 0, 0.4f){ /* During each update to the camera, we will determine whether * or not to set a new zoom factor for this camera */ @Override public void onUpdate(float pSecondsElapsed) { final float currentZoomFactor = this.getZoomFactor(); if(currentZoomFactor >= MAX_ZOOM_FACTOR){ this.setZoomFactor(MIN_ZOOM_FACTOR); } else if(currentZoomFactor <= MIN_ZOOM_FACTOR){ this.setZoomFactor(MAX_ZOOM_FACTOR); } super.onUpdate(pSecondsElapsed); } }; /* Set the initial zoom factor for camera one*/ mCameraOne.setZoomFactor(MAX_ZOOM_FACTOR); /* Create the second camera (Right half of the display) */ mCameraTwo = new SmoothCamera(0, 0, WIDTH, HEIGHT, 0, 0, 1.2f){ /* During each update to the camera, we will determine whether * or not to set a new zoom factor for this camera */ @Override public void onUpdate(float pSecondsElapsed) { final float currentZoomFactor = this.getZoomFactor(); if(currentZoomFactor >= MAX_ZOOM_FACTOR){ this.setZoomFactor(MIN_ZOOM_FACTOR); } else if(currentZoomFactor <= MIN_ZOOM_FACTOR){ this.setZoomFactor(MAX_ZOOM_FACTOR); } super.onUpdate(pSecondsElapsed); } }; /* Set the initial zoom factor for camera two */ mCameraTwo.setZoomFactor(MIN_ZOOM_FACTOR);
-
在我们
BaseGameActivity
类的onCreateEngineOptions()
方法中还需要处理一个任务,就是创建EngineOptions
对象,将mCameraOne
对象作为主相机传递。另外,场景可能需要同时处理触摸事件,因此我们也将启用多点触控:/* The first camera is set via the EngineOptions creation, as usual */ EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(), mCameraOne); /* If users should be able to control each have of the display * simultaneously with touch events, we'll need to enable * multi-touch in the engine options */ engineOptions.getTouchOptions().setNeedsMultiTouch(true);
-
在第四步中,我们将覆盖
BaseGameActivity
类的onCreateEngine()
方法,以创建一个DoubleSceneSplitScreenEngine
对象,而不是默认的Engine
对象:@Override public Engine onCreateEngine(EngineOptions pEngineOptions) { /* Return the DoubleSceneSplitScreenEngine, passing the pEngineOptions * as well as the second camera object. Remember, the first camera has * already been applied to the engineOptions which in-turn applies the * camera to the engine. */ return new DoubleSceneSplitScreenEngine(pEngineOptions, mCameraTwo); }
-
接下来,在
onCreateScene()
方法中,我们将创建两个Scene
对象,按照我们的选择设置它们,并最终将每个Scene
对象设置到DoubleSceneSplitScreenEngine
对象中:@Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { /* Create and setup the first scene */ mSceneOne = new Scene(); mSceneOne.setBackground(new Background(0.5f, 0, 0)); /* In order to keep our camera's and scenes organized, we can * set the Scene's user data to store its own camera */ mSceneOne.setUserData(mCameraOne); /* Create and setup the second scene */ mSceneTwo = new Scene(); mSceneTwo.setBackground(new Background(0,0,0.5f)); /* Same as the first Scene, we set the second scene's user data * to hold its own camera */ mSceneTwo.setUserData(mCameraTwo); /* We must set the second scene within mEngine object manually. * This does NOT need to be done with the first scene as we will * be passing it to the onCreateSceneCallback, which passes it * to the Engine object for us at the end of onCreateScene()*/ ((DoubleSceneSplitScreenEngine) mEngine).setSecondScene(mSceneTwo); /* Pass the first Scene to the engine */ pOnCreateSceneCallback.onCreateSceneFinished(mSceneOne); }
-
既然我们的两个
Camera
对象已经设置好了,两个Scene
对象也已经设置好并附加到引擎上,我们可以开始根据需要将Entity
对象附加到每个Scene
对象上,只需像往常一样指定要附加到的Scene
对象。这段代码应该放在BaseGameActivity
类的onPopulateScene()
方法中:/* Apply a rectangle to the center of the first scene */ Rectangle rectangleOne = new Rectangle(WIDTH * 0.5f, HEIGHT * 0.5f, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager()); rectangleOne.setColor(org.andengine.util.adt.color.Color.BLUE); mSceneOne.attachChild(rectangleOne); /* Apply a rectangle to the center of the second scene */ Rectangle rectangleTwo = new Rectangle(WIDTH * 0.5f, HEIGHT * 0.5f, rectangleDimensions, rectangleDimensions, mEngine.getVertexBufferObjectManager()); rectangleTwo.setColor(org.andengine.util.adt.color.Color.RED); mSceneTwo.attachChild(rectangleTwo);
它的工作原理...
使用DoubleSceneSplitScreenEngine
类时,如果我们要为多人游戏进行设置,可以假设我们的项目将需要两套所有的东西。更具体地说,我们需要为屏幕的每一半各设置两个Scene
对象以及两个Camera
对象。由于我们将每个Camera
对象的观看区域一分为二,我们将把相机的WIDTH
值减半。大多数情况下,400 像素宽和 480 像素高的相机尺寸是合理的,这使我们能够保持实体的适当透视。
在第二步中,我们设置了两个SmoothCamera
对象,它们将自动对各自场景进行放大和缩小,以为此食谱提供视觉结果。然而,DoubleSceneSplitScreenEngine
类可以使用任何Camera
对象的变体,包括最基本类型而不会导致任何问题。
在第三步中,我们继续创建EngineOptions
对象。我们提供了mCameraOne
对象作为EngineOptions
构造函数中的pCamera
参数,就像我们在任何普通实例中所做的那样。此外,我们在EngineOptions
对象中启用了多点触控,以允许同时为每个Scene
对象注册触摸事件。忽略多点触控设置将导致每个场景必须等待另一个场景没有被按下时才能注册触摸事件。
在第四步中,我们创建了DoubleSceneSplitScreenEngine
对象,传入上一步创建的pEngineOptions
参数以及第二个Camera
对象—mCameraTwo
。在代码的这个阶段,我们已经将两个摄像头注册到引擎中;第一个是在EngineOptions
对象中注册的,第二个作为参数传递给DoubleSceneSplitScreenEngine
类。
第五步包括BaseGameActivity
类的onCreateScene()
方法,在这里我们将创建并设置两个Scene
对象。在最基本的层面上,这涉及到创建Scene
对象,启用并设置或禁用场景的背景,将场景的用户数据设置为存储其相应的摄像头,并最终将Scene
对象传递给我们的mEngine
对象。虽然第二个Scene
对象需要我们调用mEngine
对象的setSecondScene(mSceneTwo)
方法,但mSceneOne
对象是像在任何BaseGameActivity
中一样传递给Engine
对象的;在pOnCreateSceneCallback.onCreateSceneFinished(mSceneOne)
方法中。
在第六步中,我们可以说已经“走出困境”。在这一点上,我们已经完成了引擎、场景和摄像头的设置,现在可以开始按照我们的喜好填充每个场景。在这一点上,我们可以做的事情的可能性非常广泛,包括将第二个场景用作小地图、多人游戏的视角、对第一个场景的另一种视角等等。此时,选择要附加Entity
对象的Scene
对象会非常简单,只需调用mSceneOne.attachChild(pEntity)
或mSceneTwo.attachChild(pEntity)
即可。
第五章:场景和图层管理
管理场景和图层对于使用菜单和多个游戏级别的游戏来说是一个必要条件。本章将介绍以下主题的场景管理器的创建和使用:
-
创建场景管理器
-
为场景资源设置资源管理器
-
定制管理的场景和图层
-
设置一个活动以使用场景管理器
简介
创建一个管理游戏菜单和场景的过程是提高框架速度的最快方法之一。一个设计良好的游戏通常依赖于强大且定制化的场景管理器来处理菜单和游戏内的关卡。定制场景管理器的方法有很多,但基础通常包括:
-
在场景之间切换
-
自动加载和卸载场景资源和元素
-
在处理场景资源和构建场景时显示加载屏幕
除了场景管理器的核心功能之外,我们还将创建一种在场景之上显示图层的方法,这样我们就可以为游戏添加另一层可用性。
创建场景管理器
创建一个仅替换引擎当前场景为另一个场景的场景管理器相当简单,但这种做法对玩家来说并不具有图形上的吸引力。在资源加载和场景构建时显示加载屏幕已经成为游戏设计中的一种广泛接受的做法,因为它让玩家知道游戏在进行的工作不仅仅只是闲置。
准备就绪...
打开本章代码包中的SceneManager.java
类。同时,也请打开ManagedScene.java
和ManagedLayer.java
类。我们将在本食谱的讨论中引用这三个类。类内的内联注释提供了关于本食谱讨论内容的额外信息。
如何操作...
按照以下步骤了解SceneManager
类的功能,以便我们可以为未来的项目创建一个定制版的场景管理器:
-
首先,请注意
SceneManager
类是作为单例创建的,这样我们就可以从项目的任何地方访问它。此外,它使用我们的ResourceManager
类提供的getEngine()
引用来存储对引擎对象的本地引用,但如果我们选择不使用资源管理器,这个引用可以在创建SceneManager
类时设置。 -
其次,注意在
getInstance()
方法之后创建的变量。前两个变量mCurrentScene
和mNextScene
保存了对当前已加载场景和将要加载的场景的引用。mEngine
变量保存了对引擎的引用。我们将使用这个引擎引用来设置我们的管理场景,以及注册/注销mLoadingScreenHandler
更新处理器。整型变量mNumFramesPassed
在更新处理器中计算已渲染的帧数,以确保加载屏幕至少显示了一帧。通过下一个变量mLoadingScreenHandler
实现显示加载屏幕的功能,我们将在下一步中更详细地了解它。其余变量用于管理图层,并跟踪图层处理过程的状态或保存与图层处理过程相关的实体引用。 -
第三,查看
mLoadingScreenHandler IUpdateHandler
更新处理器中的onUpdate()
方法。请注意,这里有两个条件块——第一个在卸载上一个场景并随后加载下一个场景之前等待一帧,而第二个则等待直到下一个场景的加载屏幕至少显示最短时间之后,它才隐藏加载屏幕并重置更新处理器使用的变量。更新处理器中的整个这个过程使得在ManagedScene
加载和构建自身时可以使用加载屏幕。 -
类中的下一个方法是
showScene()
方法,当我们想要从当前场景导航到一个后续场景时,我们将调用它。它首先将引擎相机的位置和大小重置为其起始位置和大小,以防止之前的任何相机调整破坏新场景的展示。接下来,我们通过ManagedScene
类的hasLoadingScreen
属性检查新场景是否将显示加载屏幕。如果新的
ManagedScene
类将显示加载屏幕,我们将它的子场景设置为onLoadingScreenLoadAndShown()
方法返回的场景,并暂停ManagedScene
类的所有渲染、更新和触摸事件。下面的if
块确保如果场景已经在加载阶段,可以加载新场景。这种情况应该很少见,但如果从 UI 线程调用显示新场景,则可能会发生。然后,将mNextScene
变量设置为新的ManagedScene
类,以供mLoadingScreenHandler
更新处理器和引擎的场景使用。如果新的
ManagedScene
类不显示加载屏幕,我们将mNextScene
变量设置为新的ManagedScene
类,将新的ManagedScene
类设置为引擎的场景,卸载之前显示的场景,并加载新场景。如果没有显示加载屏幕,showScene()
方法仅用于将新场景替换为之前显示的场景。 -
接下来,看看
showLayer()
方法。由于我们的层是在游戏中其他所有内容之上显示的,因此我们将它们作为相机HUD
对象的子场景进行附加。该方法首先确定相机是否有HUD
对象来附加子场景。如果有,它将mCameraHadHud
布尔值设置为true
。如果没有,我们将创建一个占位符 HUD 对象并将其设置为相机的HUD
对象。接下来,如果showLayer()
方法被调用以暂停底层ManagedScene
的渲染、更新或触摸事件,我们将设置一个占位符场景作为ManagedScene
场景的子场景,并传递给showLayer()
方法的模态属性。最后,我们将层的相机设置为引擎的相机,缩放层以匹配相机的屏幕依赖性缩放,并将局部层相关变量设置为下一步引用的hideLayer()
方法使用。 -
hideLayer()
方法首先检查当前是否有层正在显示。如果有,将清除相机HUD
对象的子场景,从ManagedScene
类中清除占位符子场景,并重置层显示系统。
按以下步骤了解ManagedScene
和ManagedLayer
类的构建方式:
-
查看
ManagedScene
类,注意类开始部分列出的变量。hasLoadingScreen
布尔值、minLoadingScreenTime
浮点数和elapsedLoadingScreenTime
浮点数变量由SceneManager
类在处理ManagedScene
类的加载屏幕时使用。isLoaded
布尔值反映了ManagedScene
类构建的完成状态。第一个构造函数是在不需要加载屏幕的情况下的便捷构造函数。第二个构造函数根据传递的值设置加载屏幕变量,这决定了加载屏幕应显示的最短时间。构造函数后面的公共方法由SceneManager
类调用,并调用适当的抽象方法,这些方法列在类的底部。 -
ManagedLayer
类与ManagedScene
类非常相似,但其固有的功能和缺少加载屏幕使其更容易创建。构造函数根据传递的pUnloadOnHidden
布尔变量设置层在隐藏后是否应该卸载。构造函数后面的公共方法调用下面的适当抽象方法。
它的工作原理...
场景管理器存储对引擎当前场景的引用。当告诉场景管理器显示一个新场景时,它会先隐藏并卸载当前场景,然后将新场景设置为当前场景。然后,如果场景有的话,它会加载并显示新场景的加载屏幕。为了在加载场景其余部分之前显示加载屏幕,我们必须允许引擎渲染一帧。mNumFramesPassed
整数值跟踪自过程开始以来发生的更新次数,也就是场景渲染次数。
在显示加载屏幕之后,或者如果不需要使用加载屏幕,场景管理器通过调用onLoadManagedScene()
让场景自行加载。加载完成后,如果存在加载屏幕,并且已经显示至少一定时间,则隐藏加载屏幕并显示场景。如果加载屏幕没有显示足够的时间,我们会暂停场景的更新,这样场景就不会在加载屏幕隐藏之前开始。要了解更多关于这个场景管理器如何处理场景切换的信息,请参考SceneManager.java
补充代码中的内联注释。
为了便于使用图层,场景管理器利用摄像头的 HUD 确保图层绘制在所有其他内容之上。如果摄像头已经有了 HUD,我们在应用图层之前先保存它,这样在图层隐藏后可以恢复原始的 HUD。此外,我们可以通过使用占位符场景来暂停底层场景的更新、渲染和触摸区域。占位符场景作为子场景附加到底层场景,因此我们必须保存底层场景已经附加的任何子场景。场景管理器通过同一方法调用来处理图层的加载和显示,让图层的子类确定是否需要重新加载,或者是否只需加载一次以减少性能负担重的加载。
另请参阅...
-
在本章中自定义管理场景和图层。
-
在本章中设置一个活动以使用场景管理器。
-
在第四章中为摄像头应用 HUD,使用摄像头。
为场景资源设置资源管理器。
为了便于菜单和游戏场景加载资源,必须首先设置资源管理器来处理这些资源。当我们调用资源管理器的loadMenuResources()
或loadGameResources()
方法时,它会自动加载相应的资源。同样,对于使用大量内存的菜单或游戏场景,卸载资源只需调用资源管理器的unloadMenuResources()
、unloadGameResources()
或unloadSharedResources()
方法。
准备就绪...
打开本章代码包中的ResourceManager.java
类,因为我们将参考它来完成这个配方。同时,也请查看该类的内联注释,以获取有关代码特定部分更多信息。
如何操作...
按照以下步骤了解ResourceManager
类是如何被设置以与我们的管理场景一起使用的:
-
注意
ResourceManager
类中定义的公共非静态变量。当加载纹理时,这个类会使用引擎和上下文变量,但它们也为我们提供了一种在整个项目中访问这些重要对象的方法。cameraWidth
、cameraHeight
、cameraScaleFactorX
和cameraScaleFactorY
变量在此类中未使用,但将在整个项目中用于相对于屏幕放置和缩放实体。 -
找到
setup()
方法。这个方法会设置前一步中引用的非静态变量,并在我们的活动类中覆盖的onCreateResources()
方法中被调用。重要的是,在调用ResourceManager
类的任何其他方法之前先调用setup()
,因为其他每个方法和变量都依赖于引擎和上下文变量。 -
接下来,看看静态资源变量。这些变量将由我们的场景用于实体或声音,并且必须在调用之前设置。还要注意,带有游戏或菜单前缀的静态变量将分别由我们的游戏或菜单场景使用,而没有前缀的静态变量将在两种类型之间共享。
-
现在找到
loadGameResources()
和loadMenuResources()
方法。当我们的管理游戏和菜单场景首次启动时,将调用这些方法。这些方法的重要职责是调用后续的ResourceManager
方法,这些方法设置前一步中引用的静态变量。相反,unloadGameResources()
和unloadMenuResources()
卸载其各自场景的资源,并且当应用程序流程完成资源使用后应调用它们。
工作原理...
在最基本的层面上,资源管理器提供了加载和卸载资源的手段。除此之外,我们定义了一系列变量,包括引擎和上下文对象,这让我们在创建场景中的实体时能够轻松访问游戏的某些常见元素。这些变量也可以放在游戏管理器或对象工厂中,但由于大多数对资源管理器的调用都接近于创建实体的代码,因此我们将其包含在资源管理器中。
另请参阅...
-
在第一章,AndEngine 游戏结构中创建资源管理器。
-
在第一章,AndEngine 游戏结构中创建游戏管理器。
-
在第一章,AndEngine 游戏结构中创建对象工厂。
定制管理场景和图层
场景管理器的主要目的是处理我们游戏中的管理场景。这些管理场景是高度可定制的,但我们希望尽可能避免重写我们的代码。为了完成这项任务,我们将使用两个扩展了 ManagedScene
类的类,ManagedGameScene
和 ManagedMenuScene
。通过这种方式构建我们的场景类,我们将拥有共享通用基础的菜单和游戏场景。
准备就绪...
打开本章代码包中的以下类:ManagedMenuScene.java
、ManagedGameScene.java
、MainMenu.java
、GameLevel.java
和 OptionsLayer.java
。我们将在本食谱中多次引用这些类。
如何操作...
按照以下步骤了解 ManagedMenuScene
和 ManagedGameScene
类是如何基于 ManagedScene
类构建的,以创建可定制的、可扩展的场景,并将其传递给 SceneManager
类:
-
查看
ManagedMenuScene
类。它只包含两个简单的构造函数和一个重写的onUnloadManagedScene()
方法。重写的方法防止了isLoaded
布尔值被设置,因为我们将不会利用场景管理器的自动卸载菜单场景功能。 -
现在,我们将注意力转向
ManagedGameScene
类。这个类首先创建了一个游戏内HUD
对象、一个加载屏幕Text
对象以及一个加载屏幕Scene
对象。ManagedGameScene
类的主构造函数首先将场景的触摸事件绑定设置设为真。接下来,设置场景的缩放以镜像摄像机的屏幕依赖性缩放,并将场景的位置设为摄像机的底部中心。最后,构造函数设置 HUD 的缩放以匹配摄像机的缩放。ManagedGameScene
类重写了ManagedScene
类的onLoadingScreenLoadAndShown()
和onLoadingScreenUnloadAndHidden()
方法,以显示和隐藏一个简单的加载屏幕,该屏幕显示一个单一的Text
对象。ManagedScene
类的onLoadScene()
方法被重写,以构建一个表示游戏内部分的场景,该场景包含一个背景和两个按钮,允许玩家返回MainMenu
或显示OptionsLayer
。
按照以下步骤了解如何扩展 ManagedMenuScene
和 ManagedGameScene
类以创建 MainMenu
和 GameLevel
场景:
-
MainMenu
类被设计为单例模式,以防止创建类的多个实例从而占用宝贵的内存空间。同时,它省略了加载屏幕,因为它几乎是瞬间加载和创建的。构成MainMenu
类的所有实体都被定义为类级别变量,包括背景、按钮、文本和移动的实体。MainMenu
类从ManagedScene
类继承的场景流程方法有onLoadScene()
、onShowScene()
、onHideScene()
和onUnloadScene()
,其中只有onLoadScene()
方法包含代码。onLoadScene()
方法加载并构建了一个场景,包括一个背景、20 个水平移动的云朵、一个标题和两个按钮。注意,每个按钮都会调用场景管理器——播放按钮显示GameLevel
场景,选项按钮显示OptionsLayer
。 -
GameLevel
类扩展了ManagedGameScene
类,并只覆盖了onLoadScene()
方法,在场景中创建并随机定位一个正方形矩形。这表明ManagedGameScene
类构成了GameLevel
类的大部分内容,而且不同级别之间的元素仍然可以使用由ManagedGameScene
类创建的相同基础。
按照以下步骤了解OptionsLayer
类是如何扩展ManagedLayer
类的层功能的:
-
关于
OptionsLayer
类,首先注意它被定义为单例,这样在首次创建后它将保留在内存中。接下来,注意两个更新处理器SlideIn
和SlideOut
。这些处理器在显示或隐藏层时为层添加动画效果,并为游戏提供额外的图形兴趣层。更新处理器只是简单地将层移动到onUpdate()
方法的pSecondsElapsed
参数成比例的特定位置,以使移动平滑。 -
从
ManagedLayer
类继承的onLoadLayer()
方法被覆盖,以创建一个作为层背景的黑色矩形和两个显示标题和退出层方式的Text
对象。onShowLayer()
和onHideLayer()
方法向引擎注册适当的更新处理器。当层滑出屏幕时,注意SlideOut
更新处理器调用场景管理器隐藏层——这就是使用这个特定场景管理器的框架实现结束动画的方式。
它是如何工作的...
ManagedMenuScene
类的唯一目的是覆盖从ManagedScene
类继承的onUnloadManagedScene()
方法,以防止场景内实体的重新创建。注意在扩展ManagedMenuScene
的MainMenu
类中覆盖的onUnloadScene()
方法,我们将其留空以确保MainMenu
类保留在内存中,这样我们可以从游戏场景和其他菜单快速切换回它。
注意
在运行此项目时,如果主菜单中有任何动画,请注意,当另一个场景正在显示时,动画会暂停,但一旦主菜单再次显示,动画就会恢复。这是因为尽管主菜单仍然加载在内存中,但它不会作为引擎的当前场景进行更新。
ManagedGameScene
类使用一个HUD
对象,允许游戏关卡拥有一组与引擎摄像机一起移动的控件。尽管在这个例子中我们将按钮添加到GameHud
对象,但 HUD 上可以使用任何控件。我们为ManagedGameScene
类使用的构造函数设置了加载屏幕的持续时间、触摸选项以及游戏场景和GameHud
的比例,以提升游戏在不同设备上的视觉吸引力。对于游戏场景,我们利用场景管理器启用的加载屏幕。对于加载屏幕,我们创建了一个简单的场景,显示文本Loading...,但可以使用任何非动画实体的排列。当显示加载屏幕时,我们加载游戏资源并创建游戏场景。在这种情况下,一个简单的背景由单个精灵构建,屏幕上的控件被添加到GameHud
对象。请注意,添加到GameHud
对象的控件会被缩放到摄像机比例因子的倒数。这是必要的,因为我们要使它们在所有设备上具有相同的物理尺寸。在ManagedGameScene
类中定义的最后一个方法是onUnloadScene()
,用于卸载场景。
备注
注意,我们所有的卸载操作都是在更新线程中完成的。这样做可以防止引擎尝试处理在当前线程中早已移除的实体,并防止抛出ArrayIndexOutOfBoundsException
异常。
对于主菜单,我们不需要加载屏幕,因此在onLoadingScreenLoadAndShown()
方法中直接返回null
。在为主菜单创建简单的精灵背景时,我们必须将其缩放以填满屏幕。注意主菜单在创建精灵和按钮时是如何使用ResourceManager
类中的菜单资源的。同样,注意点击按钮时,我们会调用SceneManager
类来加载下一个场景或显示一个图层。以下两张截图展示了主菜单在两个不同设备上的显示效果,以演示摄像机缩放如何与场景组合一起工作。第一张截图是在 10.1 英寸的摩托罗拉 Xoom 上:
第二张是在 5.3 英寸的三星 Galaxy Note 上:
我们的GameLevel
类与其超类ManagedGameScene
相比相对较小,这是因为我们希望每个关卡只包含各自所需的信息。以下屏幕截图展示了GameLevel
类在实际中的使用情况:
OptionsLayer
类可以从任何场景中显示,如下两张截图所示。第一张是在主菜单上:
当第二个游戏级别加载了GameLevel
类时:
另请参阅...
-
在本章中创建场景管理器。
-
在第四章,使用相机工作中连接控制器到显示。
设置活动以使用场景管理器
由于我们的场景管理器的工作方式,将其实例化以供扩展了 AndEngine 的BaseGameActivity
类的Activity
类使用需要很少的努力。我们还将实现一个精确的屏幕分辨率缩放方法,以确保所有设备上的外观一致性。SceneManager
类和ManagedScenes
类依赖在ResourceManager
类中定义的变量来注册更新处理程序和创建实体。在查看这个指南时,请注意我们在使用SceneManager
类的任何功能之前设置ResourceManager
类。
准备工作...
创建一个扩展了 AndEngine 的BaseGameActivity
类的新活动,或者加载你已经创建的一个。将现有活动适配为使用场景管理器需要与新建活动相同的步骤,因此不必担心重新开始一个项目以实现场景管理器。
如何操作...
按以下步骤准备一个活动以使用我们的场景管理器:
-
在你的活动中定义以下变量以处理精确的屏幕分辨率缩放。这样做可以使屏幕元素在所有安卓设备上几乎物理上一致:
static float DESIGN_SCREEN_WIDTH_PIXELS = 800f; static float DESIGN_SCREEN_HEIGHT_PIXELS = 480f; static float DESIGN_SCREEN_WIDTH_INCHES = 4.472441f; static float DESIGN_SCREEN_HEIGHT_INCHES = 2.805118f; static float MIN_WIDTH_PIXELS = 320f, MIN_HEIGHT_PIXELS = 240f; static float MAX_WIDTH_PIXELS = 1600f, MAX_HEIGHT_PIXELS = 960f; public float cameraWidth; public float cameraHeight; public float actualScreenWidthInches; public float actualScreenHeightInches;
-
在活动类的相应位置添加以下方法来处理返回按钮:
public boolean onKeyDown(final int keyCode, final KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { if(ResourceManager.getInstance().engine!=null){ if(SceneManager.getInstance().isLayerShown) SceneManager.getInstance(). currentLayer.onHideLayer(); else if( SceneManager.getInstance(). mCurrentScene.getClass(). getGenericSuperclass(). equals(ManagedGameScene.class) || (SceneManager.getInstance(). mCurrentScene.getClass(). getGenericSuperclass(). equals(ManagedMenuScene.class) &! SceneManager.getInstance(). mCurrentScene.getClass(). equals(MainMenu.class))) SceneManager.getInstance(). showMainMenu(); else System.exit(0); } return true; } else { return super.onKeyDown(keyCode, event); } }
-
接下来,用以下代码填充
onCreateEngineOptions()
方法:actualScreenWidthInches = getResources(). getDisplayMetrics().widthPixels / getResources().getDisplayMetrics().xdpi; actualScreenHeightInches = getResources(). getDisplayMetrics().heightPixels / getResources().getDisplayMetrics().ydpi; cameraWidth = Math.round( Math.max( Math.min( DESIGN_SCREEN_WIDTH_PIXELS * (actualScreenWidthInches / DESIGN_SCREEN_WIDTH_INCHES), MAX_WIDTH_PIXELS), MIN_WIDTH_PIXELS)); cameraHeight = Math.round( Math.max( Math.min( DESIGN_SCREEN_HEIGHT_PIXELS * (actualScreenHeightInches / DESIGN_SCREEN_HEIGHT_INCHES), MAX_HEIGHT_PIXELS), MIN_HEIGHT_PIXELS)); EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)); engineOptions.getAudioOptions().setNeedsSound(true); engineOptions.getAudioOptions().setNeedsMusic(true); engineOptions.getRenderOptions().setDithering(true); engineOptions.getRenderOptions(). getConfigChooserOptions().setRequestedMultiSampling(true); engineOptions.setWakeLockOptions(WakeLockOptions.SCREEN_ON); return engineOptions;
-
在
onCreateResources()
方法中放置以下行:ResourceManager.getInstance().setup(this.getEngine(), this.getApplicationContext(), cameraWidth, cameraHeight, cameraWidth/DESIGN_SCREEN_WIDTH_PIXELS, cameraHeight/DESIGN_SCREEN_HEIGHT_PIXELS);
-
最后,在
onCreateScene()
方法中添加以下代码:SceneManager.getInstance().showMainMenu(); pOnCreateSceneCallback.onCreateSceneFinished( MainMenu.getInstance());
工作原理...
我们首先定义开发设备屏幕的属性,以便我们可以进行计算,确保所有玩家尽可能接近我们看待游戏的方式。实际上,计算是在第三步中展示的onCreateEngineOptions()
方法中处理的。对于引擎选项,我们启用了声音、音乐、平滑渐变的抖动处理、平滑边缘的多重采样以及防止玩家短暂切换到其他应用时游戏资源被销毁的唤醒锁定。
在第 4 步中,我们通过传递Engine
对象、Context
、当前相机宽度和高度以及当前相机尺寸与设计设备屏幕尺寸的比例来设置ResourceManager
类。最后,我们告诉SceneManager
类显示主菜单,并通过pOnCreateSceneCallback
参数将MainMenu
类作为引擎的场景传递。
另请参阅...
-
在本章中创建场景管理器。
-
在第一章,AndEngine 游戏结构中了解生命周期。
第六章:物理学的应用
基于物理的游戏为玩家提供了许多其他类型无法体验的独特体验。本章介绍了 AndEngine 的 Box2D 物理扩展 的使用。我们的食谱包括:
-
Box2D 物理扩展简介
-
理解不同的物体类型
-
创建分类过滤的物体
-
创建多固定装置物体
-
通过指定顶点创建独特的物体
-
使用力、速度和扭矩
-
对特定物体应用反重力
-
与关节一起工作
-
创建布娃娃
-
创建绳子
-
与碰撞工作
-
使用 preSolve 和 postSolve
-
创建可破坏的物体
-
射线投射
Box2D 物理扩展简介
基于物理的游戏是移动设备上最受欢迎的游戏类型之一。AndEngine 允许使用 Box2D 扩展来创建基于物理的游戏。通过这个扩展,我们可以构建任何类型的物理现实的 2D 环境,从小的简单模拟到复杂游戏。在本食谱中,我们将创建一个演示简单设置的活动,以利用 Box2D 物理引擎扩展。此外,我们将在本章的剩余食谱中使用此活动。
准备就绪...
首先,创建一个名为 PhysicsApplication
的新活动类,该类扩展了 BaseGameActivity
并实现了 IAccelerationListener
和 IOnSceneTouchListener
。
如何操作...
按照以下步骤构建我们的 PhysicsApplication
活动类:
-
在类中创建以下变量:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public FixedStepPhysicsWorld mPhysicsWorld; public Body groundWallBody; public Body roofWallBody; public Body leftWallBody; public Body rightWallBody;
-
我们需要建立活动的基础。为此,首先在类中放置这四个常见的重写方法,以设置引擎、资源和主场景:
@Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)); engineOptions.getRenderOptions().setDithering(true); engineOptions.getRenderOptions(). getConfigChooserOptions() .setRequestedMultiSampling(true); engineOptions.setWakeLockOptions( WakeLockOptions.SCREEN_ON); return engineOptions; } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback. onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
继续设置活动,通过添加以下重写方法,该方法将用于填充我们的场景:
@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { }
-
接下来,我们将用以下代码填充前一个方法,以创建我们的
PhysicsWorld
对象和Scene
对象:mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0f,-SensorManager.GRAVITY_EARTH*2), false, 8, 3); mScene.registerUpdateHandler(mPhysicsWorld); final FixtureDef WALL_FIXTURE_DEF = PhysicsFactory.createFixtureDef(0, 0.1f, 0.5f); final Rectangle ground = new Rectangle(cameraWidth / 2f, 6f, cameraWidth - 4f, 8f, this.getVertexBufferObjectManager()); final Rectangle roof = new Rectangle(cameraWidth / 2f, cameraHeight – 6f, cameraWidth - 4f, 8f, this.getVertexBufferObjectManager()); final Rectangle left = new Rectangle(6f, cameraHeight / 2f, 8f, cameraHeight - 4f, this.getVertexBufferObjectManager()); final Rectangle right = new Rectangle(cameraWidth - 6f, cameraHeight / 2f, 8f, cameraHeight - 4f, this.getVertexBufferObjectManager()); ground.setColor(0f, 0f, 0f); roof.setColor(0f, 0f, 0f); left.setColor(0f, 0f, 0f); right.setColor(0f, 0f, 0f); groundWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, ground, BodyType.StaticBody, WALL_FIXTURE_DEF); roofWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, roof, BodyType.StaticBody, WALL_FIXTURE_DEF); leftWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, left, BodyType.StaticBody, WALL_FIXTURE_DEF); rightWallBody = PhysicsFactory.createBoxBody( this.mPhysicsWorld, right, BodyType.StaticBody, WALL_FIXTURE_DEF); this.mScene.attachChild(ground); this.mScene.attachChild(roof); this.mScene.attachChild(left); this.mScene.attachChild(right); // Further recipes in this chapter will require us to place code here. mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished();
-
以下重写活动处理场景触摸事件、加速度计输入以及两个引擎生命周期事件—
onResumeGame
和onPauseGame
。将它们放在类的末尾以完成此食谱:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { // Further recipes in this chapter will require us to place code here. return true; } @Override public void onAccelerationAccuracyChanged( AccelerationData pAccelerationData) {} @Override public void onAccelerationChanged( AccelerationData pAccelerationData) { final Vector2 gravity = Vector2Pool.obtain( pAccelerationData.getX(), pAccelerationData.getY()); this.mPhysicsWorld.setGravity(gravity); Vector2Pool.recycle(gravity); } @Override public void onResumeGame() { super.onResumeGame(); this.enableAccelerationSensor(this); } @Override public void onPauseGame() { super.onPauseGame(); this.disableAccelerationSensor(); }
工作原理...
我们首先要定义一个相机的宽度和高度。然后,我们定义一个 Scene
对象和一个 FixedStepPhysicsWorld
对象,在其中进行物理模拟。最后一组变量定义了作为我们基于物理的场景边界的对象。
在第二步中,我们重写了onCreateEngine()
方法,以返回一个每秒处理60
次更新的FixedStepEngine
对象。我们这样做的同时还使用了一个FixedStepPhysicsWorld
对象,是为了创建一个在所有设备上都能保持一致的模拟,而不管设备处理物理模拟的效率如何。然后我们使用标准偏好创建EngineOptions
对象,仅用一个简单的回调创建onCreateResources()
方法,并将主场景设置为浅灰色背景。
在onPopulateScene()
方法中,我们创建了一个FixedStepPhysicsWorld
对象,其重力是地球的两倍,通过(x,y)
坐标的Vector2
对象传递,并且每秒更新60
次。重力可以被设置为其他值以使模拟更加真实,或者设置为0
以创建零重力模拟。重力设置为0
对于太空模拟或者使用俯视摄像机视角而不是侧视视角的游戏很有用。布尔参数false
设置了PhysicsWorld
对象的AllowSleep
属性,告诉PhysicsWorld
在停止后不要让任何实体自行停用。FixedStepPhysicsWorld
对象的最后两个参数告诉物理引擎计算速度和位置移动的次数。更高的迭代次数将创建更准确的模拟,但也可能因为处理器负载增加而导致延迟或抖动。在创建FixedStepPhysicsWorld
对象之后,我们将其注册为主场景的更新处理器。未经注册,物理世界不会运行模拟。
变量WALL_FIXTURE_DEF
是一个固定装置定义。固定装置定义包含了将在物理世界中作为固定装置创建的实体的形状和材质属性。固定装置的形状可以是圆形或多边形的。固定装置的材质通过其密度、弹性和摩擦系数来定义,这些都是在创建固定装置定义时需要提供的。在创建WALL_FIXTURE_DEF
变量之后,我们创建了四个矩形,它们将代表墙壁实体的位置。在 Box2D 物理世界中,一个实体是由固定装置组成的。虽然只需要一个固定装置来创建一个实体,但多个固定装置可以创建具有不同属性的复杂实体。
在onPopulateScene()
方法的后续部分,我们创建了将作为物理世界中的墙壁的盒子实体。之前创建的矩形被传递给这些实体以定义它们的位置和形状。然后我们将这些实体定义为静态的,这意味着它们在物理模拟中不会对任何力产生反应。最后,我们将墙壁固定装置定义传递给实体以完成它们的创建。
创建刚体后,我们将矩形附加到主场景,并将场景的触摸监听器设置为我们活动,该活动将通过 onSceneTouchEvent()
方法访问。onPopulateScene()
方法中的最后一行告诉引擎场景已准备好显示。
重写的 onSceneTouchEvent()
方法将处理我们场景的所有触摸交互。onAccelerationAccuracyChanged()
和 onAccelerationChanged()
方法继承自 IAccelerationListener
接口,允许我们在设备倾斜、旋转或平移时改变物理世界的重力。我们重写 onResumeGame()
和 onPauseGame()
方法,以防止游戏活动不在前台时加速计使用不必要的电池电量。
还有更多...
在重写的 onAccelerationChanged()
方法中,我们两次调用了 Vector2Pool
类。Vector2Pool
类只是为我们提供了一种复用 Vector2
对象的方法,否则这些对象可能需要系统进行垃圾回收。在较新的设备上,Android 垃圾收集器已经得到优化,以减少明显的卡顿,但较旧的设备可能会根据被垃圾回收的变量占用的内存量仍然出现延迟。
注意
访问 www.box2d.org/manual.html
查看完整的Box2D 用户手册。AndEngine Box2D 扩展基于官方 Box2D C++ 物理引擎的 Java 移植版本,因此在程序上存在一些差异,但总体概念仍然适用。
另请参阅
- 了解本章中的不同身体类型。
了解不同的身体类型
Box2D 物理世界为我们提供了创建不同身体类型的方法,使我们能够控制物理模拟。我们可以生成动态刚体,它们会对力和其他刚体做出反应;静态刚体,它们不会移动;以及运动刚体,它们会移动但不受力或其他刚体的影响。选择每个刚体的类型对于产生准确的物理模拟至关重要。在本教程中,我们将看到三种不同身体类型的刚体在碰撞期间如何相互反应。
准备工作...
按照本章开始部分给出的 Box2D 物理扩展介绍 部分的教程,创建一个新的活动,以便创建具有不同身体类型的刚体。
如何操作...
完成以下步骤,了解为刚体指定不同的身体类型如何影响它们:
-
首先,在
onPopulateScene()
方法中插入以下固定定义:FixtureDef BoxBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0f, 0.5f);
-
接下来,在上一步的固定定义之后放置以下代码,创建三个矩形及其对应的刚体:
Rectangle staticRectangle = new Rectangle(cameraWidth / 2f,75f,400f,40f,this.getVertexBufferObjectManager()); staticRectangle.setColor(0.8f, 0f, 0f); mScene.attachChild(staticRectangle); PhysicsFactory.createBoxBody(mPhysicsWorld, staticRectangle, BodyType.StaticBody, BoxBodyFixtureDef); Rectangle dynamicRectangle = new Rectangle(400f, 120f, 40f, 40f, this.getVertexBufferObjectManager()); dynamicRectangle.setColor(0f, 0.8f, 0f); mScene.attachChild(dynamicRectangle); Body dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRectangle, BodyType.DynamicBody, BoxBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRectangle, dynamicBody); Rectangle kinematicRectangle = new Rectangle(600f, 100f, 40f, 40f, this.getVertexBufferObjectManager()); kinematicRectangle.setColor(0.8f, 0.8f, 0f); mScene.attachChild(kinematicRectangle); Body kinematicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, kinematicRectangle, BodyType.KinematicBody, BoxBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( kinematicRectangle, kinematicBody);
-
最后,在上一步定义之后添加以下代码,为我们的运动刚体设置线性和角速度:
kinematicBody.setLinearVelocity(-2f, 0f); kinematicBody.setAngularVelocity((float) (-Math.PI));
工作原理...
在第一步中,我们创建了BoxBodyFixtureDef
夹具定义,我们将在第二步创建刚体时使用它。有关夹具定义的更多信息,请参阅本章中的Box2D 物理扩展介绍食谱。
在第二步中,我们首先通过调用Rectangle
构造函数来定义staticRectangle
矩形。我们将staticRectangle
放置在场景的下方中央位置,坐标为cameraWidth / 2f, 75f
,并设置矩形的宽度为400f
,高度为40f
,使其成为一条长条形平板。然后,我们通过调用staticRectangle.setColor(0.8f, 0f, 0f)
将staticRectangle
矩形的颜色设置为红色。最后,对于staticRectangle
矩形,我们通过调用mScene.attachChild()
方法,并将staticRectangle
作为参数,将其附加到场景中。接下来,我们在物理世界中创建一个与staticRectangle
相匹配的刚体。为此,我们调用PhysicsFactory.createBoxBody()
方法,参数包括mPhysicsWorld
(我们的物理世界)、staticRectangle
(告诉箱子以与staticRectangle
矩形相同的位置和大小创建)、BodyType.StaticBody
(将刚体定义为静态)以及我们的BoxBodyFixtureDef
夹具定义。
我们下一个矩形dynamicRectangle
在位置400f
和120f
处创建,位于场景的中央,略高于staticRectangle
矩形。我们的dynamicRectangle
矩形的宽度和高度设置为40f
,使其成为一个小的正方形。然后,我们通过调用dynamicRectangle.setColor(0f, 0.8f, 0f)
将其颜色设置为绿色,并通过mScene.attachChild(dynamicRectangle)
将其附加到我们的场景中。接下来,我们使用与staticRectangle
矩形相同的方式,通过PhysicsFactory.createBoxBody()
方法创建dynamicBody
变量。注意,我们将dynamicBody
变量的BodyType
设置为DynamicBody
。这会将刚体设置为动态的。现在,我们注册PhysicsConnector
与物理世界,将dynamicRectangle
和dynamicBody
连接起来。PhysicsConnecter
类将场景中的实体与物理世界中的刚体相连接,表示刚体在场景中的实时位置和旋转。
我们最后的矩形kinematicRectangle
在位置600f
和100f
处创建,这将其放置在我们场景右侧的staticRectangle
矩形上方。它被设置为具有40f
的高度和宽度,使其成为像我们的dynamicRectangle
矩形那样的小正方形。然后我们将kinematicRectangle
矩形的颜色设置为黄色并附加到我们的场景中。与我们之前创建的两个物体类似,我们调用PhysicsFactory.createBoxBody()
方法来创建我们的kinematicBody
变量。请注意,我们使用BodyType
类型为KinematicBody
的参数来创建kinematicBody
变量。这将其设置为运动学物体,因此只能通过设置其速度来进行移动。最后,我们在kinematicRectangle
矩形和kinematicBody
物体类型之间注册一个PhysicsConnector
类。
在最后一步中,我们通过调用setLinearVelocity()
方法并设置 x 轴上的-2f
向量来设置kinematicBody
物体的线性速度,使其向左移动。最后,我们通过调用kinematicBody.setAngularVelocity((float) (-Math.PI))
将kinematicBody
物体的角速度设置为负的π。有关设置物体速度的更多信息,请参见本章节中的使用力、速度和扭矩食谱。
还有更多内容...
静态物体不能通过施加或设定的力来移动,但可以使用setTransform()
方法进行重新定位。然而,在模拟运行时,我们应避免使用setTransform()
方法,因为它会使模拟变得不稳定,并可能导致一些奇怪的行为。相反,如果我们想要改变静态物体的位置,可以在创建模拟时进行,如果需要在运行时改变位置,只需检查新的位置是否会导致静态物体与现有的动态物体或运动学物体重叠。
运动学物体不能施加力,但我们可以通过setLinearVelocity()
和setAngularVelocity()
方法设置它们的速度。
另请参阅
-
本章节中的Box2D 物理扩展介绍。
-
本章节中的使用力、速度和扭矩。
创建分类筛选的物体
根据我们想要实现的物理模拟类型,控制哪些物体能够发生碰撞可能非常有用。在 Box2D 中,我们可以为夹具分配一个类别和类别筛选器,以控制哪些夹具可以互动。本食谱将介绍两个定义了类别筛选的夹具,这些夹具将通过触摸场景创建的物体来演示类别筛选。
准备好了...
按照本章开始部分给出的Box2D 物理扩展介绍部分的步骤创建一个活动。这个活动将促进本节中使用的分类筛选物体的创建。
如何操作...
按照以下步骤构建我们的分类筛选演示活动:
-
在活动中定义以下类级别变量:
private int mBodyCount = 0; public static final short CATEGORYBIT_DEFAULT = 1; public static final short CATEGORYBIT_RED_BOX = 2; public static final short CATEGORYBIT_GREEN_BOX = 4; public static final short MASKBITS_RED_BOX = CATEGORYBIT_DEFAULT + CATEGORYBIT_RED_BOX; public static final short MASKBITS_GREEN_BOX = CATEGORYBIT_DEFAULT + CATEGORYBIT_GREEN_BOX; public static final FixtureDef RED_BOX_FIXTURE_DEF = PhysicsFactory.createFixtureDef(1, 0.5f, 0.5f, false, CATEGORYBIT_RED_BOX, MASKBITS_RED_BOX, (short)0); public static final FixtureDef GREEN_BOX_FIXTURE_DEF = PhysicsFactory.createFixtureDef(1, 0.5f, 0.5f, false, CATEGORYBIT_GREEN_BOX, MASKBITS_GREEN_BOX, (short)0);
-
接下来,在类中创建此方法,以在给定位置生成新的类别筛选刚体:
private void addBody(final float pX, final float pY) { this.mBodyCount++; final Rectangle rectangle = new Rectangle(pX, pY, 50f, 50f, this.getVertexBufferObjectManager()); rectangle.setAlpha(0.5f); final Body body; if(this.mBodyCount % 2 == 0) { rectangle.setColor(1f, 0f, 0f); body = PhysicsFactory.createBoxBody(this.mPhysicsWorld, rectangle, BodyType.DynamicBody, RED_FIXTURE_DEF); } else { rectangle.setColor(0f, 1f, 0f); body = PhysicsFactory.createBoxBody(this.mPhysicsWorld, rectangle, BodyType.DynamicBody, GREEN_FIXTURE_DEF); } this.mScene.attachChild(rectangle); this.mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( rectangle, body, true, true)); }
-
最后,用以下代码填充
onSceneTouchEvent()
方法的主体,该代码通过传递触摸位置来调用addBody()
方法:if(this.mPhysicsWorld != null) if(pSceneTouchEvent.isActionDown()) this.addBody(pSceneTouchEvent.getX(), pSceneTouchEvent.getY());
它的工作原理是...
在第一步中,我们创建了一个整数mBodyCount
,用于计算我们向物理世界添加了多少个刚体。mBodyCount
整数在第二步中用于确定应将哪种颜色(从而确定哪个类别)分配给新的刚体。
我们通过定义具有唯一幂次方的短整数来创建CATEGORYBIT_DEFAULT
、CATEGORYBIT_RED_BOX
和CATEGORYBIT_GREEN_BOX
类别位,以及通过将相关的类别位相加来定义MASKBITS_RED_BOX
和MASKBITS_GREEN_BOX
掩码位。类别位用于为固定装置分配一个类别,而掩码位结合不同的类别位以确定固定装置可以与哪些类别发生碰撞。然后,我们将类别位和掩码位传递给固定装置定义以创建具有类别碰撞规则的固定装置。
第二步是一个简单的方法,用于创建一个矩形及其对应的刚体。该方法采用我们想要用来创建新刚体的X
和Y
位置参数,并将它们传递给Rectangle
对象的构造函数,同时我们还传递了一个高度和宽度为50f
以及活动的VertexBufferObjectManager
。然后,我们使用rectangle.setAlpha()
方法将矩形设置为 50%透明。之后,我们定义一个刚体,并通过将mBodyCount
变量与2
取模来确定每个创建的刚体的颜色和装置。确定颜色和装置后,我们通过设置矩形的颜色和创建一个刚体来分配它们,传递我们的mPhysicsWorld
物理世界、矩形、动态刚体类型以及先前确定的装置。最后,我们将矩形附加到我们的场景中,并注册一个PhysicsConnector
类以将矩形连接到我们的刚体。
第三步仅当已创建物理世界且场景的TouchEvent
为ActionDown
时,才从第二步调用addBody()
方法。传递的参数pSceneTouchEvent.getX()
和pSceneTouchEvent.getY()
表示在场景上接收触摸输入的位置,这也是我们想要创建新的类别筛选刚体的位置。
还有更多...
所有固定装置的默认类别值为一。在为特定固定装置创建掩码位时,请记住,包含默认类别的任何组合都会导致该固定装置与所有未设置为避免与之碰撞的其他固定装置发生碰撞。
另请参阅
-
本章介绍Box2D 物理扩展。
-
本章中了解不同的刚体类型。
创建具有多个装置的刚体。
有时我们需要一个具有不同物理属性的身体部位。例如,带保险杠的车如果撞墙应该与没有保险杠的车有不同的反应。在 Box2D 中创建这样的多固定装置身体是相当简单直接的。在本节中,我们将了解如何通过创建两个固定装置并将它们添加到空身体中来创建多固定装置身体。
准备工作...
按照本章开始部分Box2D 物理扩展介绍一节中的步骤创建一个新的活动,以便促进我们多固定装置身体的创建。
如何操作...
按照以下步骤,了解如何创建多固定装置身体:
-
在
onPopulateScene()
方法中放置以下代码,以创建两个具有修改过的AnchorCenter
值的矩形,这允许在连接到身体时进行精确放置:Rectangle nonbouncyBoxRect = new Rectangle(0f, 0f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); nonbouncyBoxRect.setColor(0f, 0f, 0f); nonbouncyBoxRect.setAnchorCenter(((nonbouncyBoxRect.getWidth() / 2) - nonbouncyBoxRect.getX()) / nonbouncyBoxRect.getWidth(), ((nonbouncyBoxRect.getHeight() / 2) – nonbouncyBoxRect.getY()) / nonbouncyBoxRect.getHeight()); mScene.attachChild(nonbouncyBoxRect); Rectangle bouncyBoxRect = new Rectangle(0f, -55f, 90f, 10f, this.getEngine().getVertexBufferObjectManager()); bouncyBoxRect.setColor(0f, 0.75f, 0f); bouncyBoxRect.setAnchorCenter(((bouncyBoxRect.getWidth() / 2) – bouncyBoxRect.getX()) / bouncyBoxRect.getWidth(), ((bouncyBoxRect.getHeight() / 2) – bouncyBoxRect.getY()) / bouncyBoxRect.getHeight()); mScene.attachChild(bouncyBoxRect);
-
以下代码创建了一个
Body
对象和两个固定装置,一个完全弹性,另一个完全非弹性。在前面步骤中创建矩形后添加它:Body multiFixtureBody = mPhysicsWorld.createBody(new BodyDef()); multiFixtureBody.setType(BodyType.DynamicBody); FixtureDef nonbouncyBoxFixtureDef = PhysicsFactory.createFixtureDef(20, 0.0f, 0.5f); final PolygonShape nonbouncyBoxShape = new PolygonShape(); nonbouncyBoxShape.setAsBox((nonbouncyBoxRect.getWidth() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (nonbouncyBoxRect.getHeight() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, new Vector2(nonbouncyBoxRect.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, nonbouncyBoxRect.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), 0f); nonbouncyBoxFixtureDef.shape = nonbouncyBoxShape; multiFixtureBody.createFixture(nonbouncyBoxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( nonbouncyBoxRect, multiFixtureBody)); FixtureDef bouncyBoxFixtureDef = PhysicsFactory.createFixtureDef(20, 1f, 0.5f); final PolygonShape bouncyBoxShape = new PolygonShape(); bouncyBoxShape.setAsBox((bouncyBoxRect.getWidth() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (bouncyBoxRect.getHeight() / 2f) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, new Vector2(bouncyBoxRect.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, bouncyBoxRect.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), 0f); bouncyBoxFixtureDef.shape = bouncyBoxShape; multiFixtureBody.createFixture(bouncyBoxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( bouncyBoxRect, multiFixtureBody));
-
最后,我们需要设置多固定装置身体的位置,既然它已经被创建了。在前面步骤中创建身体后,放置以下对
setTransform()
的调用:multiFixtureBody.setTransform(400f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, 240f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, 0f);
工作原理...
我们首先使用Rectangle
构造函数定义一个矩形,它将代表一个非弹性的固定装置,在 x 轴和 y 轴上传递0f
,表示世界原点。然后我们传递一个高度和宽度为100f
,这使得矩形成为一个大正方形,以及活动的VertexBufferObjectManager
。
接着,我们将非弹跳矩形的颜色设置为黑色 0f, 0f, 0f
,并使用 nonbouncyBoxRect.setAnchorCenter()
方法设置其锚点中心,以表示在第二步创建的物体上,非弹跳矩形将被附着的地点的位置。锚点中心的位置 (((nonbouncyBoxRect.getWidth() / 2) - nonbouncyBoxRect.getX()) / nonbouncyBoxRect.getWidth(), ((nonbouncyBoxRect.getHeight() / 2) – nonbouncyBoxRect.getY()) / nonbouncyBoxRect.getHeight()
将矩形的定位和大小转换为相对于原点的位置。在我们的非弹跳矩形的情况下,锚点中心保持在默认的 0.5f, 0.5f
,但对于任何从非原点中心矩形创建的固定装置,这个公式是必要的。接下来,我们将非弹跳矩形附加到场景中。然后,我们使用与非弹跳矩形相同的方法创建一个将表示弹跳组件的矩形,但是我们将矩形在 y 轴上放置在 -55f
的位置,使其直接位于非弹跳矩形的下方。我们还将矩形的宽度设置为 90f
,使其比之前的矩形略小,并将高度设置为 10f
,使其成为一个细长的条,作为非弹跳矩形下方的弹跳部分。使用与非弹跳矩形相同的公式设置弹跳矩形的锚点中心后,我们将其附加到场景中。请注意,我们已经修改了每个矩形的 AnchorCenter
值,这样在第二步中注册的 PhysicsConnectors
类可以在运行模拟时将矩形放置在正确的位置。还要注意,我们在世界原点创建我们的矩形和多固定装置物体,以简化计算并提高速度。在物体创建之后,我们将其移动到模拟中应有的位置,如第三步所示,当我们调用 multiFixtureBody.setTransform()
方法时,使用参数 400f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
和 240f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
,这代表了物理世界中的屏幕中心,以及 0f
,这表示物体将具有的零旋转。
在第二步中,我们通过调用 mPhysicsWorld.createBody(new BodyDef())
创建一个空的物体 multiFixtureBody
,并通过调用其 setType()
方法并传入参数 BodyType.DynamicBody
来设置它为动态的。然后,我们定义一个用于非弹跳组件的固定装置定义 nonbouncyBoxFixtureDef
。
接下来,我们创建一个名为 nonbouncyBoxShape
的 PolygonShape
形状,并通过调用 nonbouncyBoxShape
形状的 setAsBox()
方法,将第一个两个参数设置为 nonbouncyBoxRect.getWidth() / 2f
和 nonbouncyBoxRect.getHeight() / 2f
,使其模仿我们的 nonbouncyBoxRect
矩形,从而将其设置为与 nonbouncyBoxRect
矩形具有相同的宽度和高度。这两个参数都除以 PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
以将值缩放到物理世界。此外,setAsBox()
方法的头两个参数是半尺寸。这意味着正常的宽度 10f
将作为 5f
传递给 setAsBox()
方法。setAsBox()
方法的下一个参数是一个 Vector2
参数,用于标识我们的 nonbouncyBoxShape
形状在物理世界中的位置。我们将其设置为我们的 nonbouncyBoxRect
矩形的当前位置,并通过使用 PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
变量进行缩放,将位置转换为物理世界坐标。setAsBox()
方法的最后一个参数是 nonbouncyBoxShape
应具有的旋转。因为我们的 nonbouncyBoxRect
矩形没有旋转,所以我们使用 0f
。
然后,我们将 nonbouncyBoxFixtureDef
夹具定义的 shape
属性设置为 nonbouncyBoxShape
,将形状应用到我们的夹具定义中。接下来,通过调用物体的 createFixture()
方法,并将 nonbouncyBoxFixtureDef
夹具定义作为参数,将夹具附加到我们的 multifixture 物体上。然后,我们注册一个 PhysicsConnector
类,将场景中的 nonbouncyBoxRect
矩形与物理世界中的 multiFixtureBody
物体连接起来。最后,我们按照创建非弹跳夹具时使用的相同程序来创建我们的弹跳夹具。结果应该是一个带有绿色弹跳边的黑色正方形。
注意
通过将夹具定义中的 isSensor
属性设置为 true
,可以创建一个传感器夹具,使其能够与其他夹具接触而不发生物理交互。关于传感器的更多信息,请参见 Box2D 手册中的夹具部分,链接为 www.box2d.org/manual.html
。
另请参阅
-
本章介绍 Box2D 物理扩展*。
-
本章中了解不同的物体类型。
通过指定顶点创建独特的物体。
我们物理模拟中的所有东西不必都是由矩形或圆形制成的。我们还可以通过创建多边形点的列表来创建多边形物体。这种方法对于创建特定类型的地形、车辆和角色很有用。在本教程中,我们将演示如何从顶点列表中创建一个独特的物体。
准备就绪...
按照本章开始部分给出的Box2D 物理扩展介绍一节中的步骤创建一个活动。这个活动将轻松允许创建一个具有顶点的独特构造的物体。
如何操作...
完成以下步骤以定义和创建我们独特的多边形主体:
-
我们独特主体的顶点将由
Vector2
对象列表定义。将以下列表添加到onPopulateScene()
方法中:List<Vector2> UniqueBodyVertices = new ArrayList<Vector2>(); UniqueBodyVertices.addAll((List<Vector2>) ListUtils.toList( new Vector2[] { new Vector2(-53f,-75f), new Vector2(-107f,-14f), new Vector2(-101f,41f), new Vector2(-71f,74f), new Vector2(69f,74f), new Vector2(98f,41f), new Vector2(104f,-14f), new Vector2(51f,-75f), new Vector2(79f,9f), new Vector2(43f,34f), new Vector2(-46f,34f), new Vector2(-80f,9f) }));
-
要使用前面的顶点列表,我们必须通过
EarClippingTriangulator
类处理它们,将顶点列表转换为物理引擎将用于创建多个固定装置并连接成一个单一主体的三角形列表。在初始Vector2
列表创建后放置以下代码:List<Vector2> UniqueBodyVerticesTriangulated = new EarClippingTriangulator(). computeTriangles(UniqueBodyVertices);
-
要创建表示我们独特主体的网格,以及调整三角化顶点以在物理世界中使用,请添加以下代码片段:
float[] MeshTriangles = new float[UniqueBodyVerticesTriangulated.size() * 3]; for(int i = 0; i < UniqueBodyVerticesTriangulated.size(); i++) { MeshTriangles[i*3] = UniqueBodyVerticesTriangulated.get(i).x; MeshTriangles[i*3+1] = UniqueBodyVerticesTriangulated.get(i).y; UniqueBodyVerticesTriangulated.get(i). mul(1/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); } Mesh UniqueBodyMesh = new Mesh(400f, 260f, MeshTriangles, UniqueBodyVerticesTriangulated.size(), DrawMode.TRIANGLES, this.getVertexBufferObjectManager()); UniqueBodyMesh.setColor(1f, 0f, 0f); mScene.attachChild(UniqueBodyMesh);
-
既然我们已经调整了顶点以在物理世界中使用,我们可以创建主体:
FixtureDef uniqueBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.5f, 0.5f); Body uniqueBody = PhysicsFactory.createTrianglulatedBody( mPhysicsWorld, UniqueBodyMesh, UniqueBodyVerticesTriangulated, BodyType.DynamicBody, uniqueBodyFixtureDef); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(UniqueBodyMesh, uniqueBody));
-
最后,我们希望独特主体有与之碰撞的物体。添加以下主体定义以创建两个静态主体,它们将在我们的物理世界中充当小钉子:
FixtureDef BoxBodyFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.6f, 0.5f); Rectangle Box1 = new Rectangle(340f, 160f, 20f, 20f, this.getVertexBufferObjectManager()); mScene.attachChild(Box1); PhysicsFactory.createBoxBody(mPhysicsWorld, Box1, BodyType.StaticBody, BoxBodyFixtureDef); Rectangle Box2 = new Rectangle(600f, 160f, 20f, 20f, this.getVertexBufferObjectManager()); mScene.attachChild(Box2); PhysicsFactory.createBoxBody(mPhysicsWorld, Box2, BodyType.StaticBody, BoxBodyFixtureDef);
工作原理...
我们最初创建的顶点列表表示我们独特主体的形状,相对于主体的中心。在第二步中,我们使用EarClippingTriangulator
类创建另一个顶点列表。从EarClippingTriangulator
类的computeTriangles()
方法返回的列表包含了构成我们独特主体的所有三角形点。以下图展示了在通过EarClippingTriangulator
类处理顶点之前和之后我们的多边形主体的样子。请注意,我们的主体将由表示原始形状的几个三角形形状组成:
在第三步中,将每个顶点添加到MeshTriangles
数组以用于创建表示我们主体的网格后,我们每个顶点都乘以1/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
,这相当于将顶点的坐标除以默认的像素到米比率。这种除法过程是用于将场景坐标转换为物理世界坐标的常见做法。物理世界以米为单位测量距离,因此需要从像素进行转换。任何一致且合理的值都可以用作转换常数,但默认的像素到米比率是每米 32 像素,几乎在每一个模拟中都已被证明是有效的。
第四步通过调用PhysicsFactory.createTrianglulatedBody
创建独特的主体。需要注意的是,尽管可以从非三角化的顶点列表创建多边形主体,但只有当我们的列表顶点少于七个时,这样做才会有所好处。即使列表很小,三角化主体对模拟的影响也没有明显的负面影响。
注意
有几个物理主体编辑器可以简化主体的创建。以下都是与 AndEngine 兼容的:
-
Physics Body Editor(免费):
code.google.com/p/box2d-editor
-
PhysicsEditor(付费):
www.codeandweb.com/physicseditor
-
Inkscape(免费,需要插件):
inkscape.org/
另请参阅
-
本章介绍Box2D 物理扩展。
-
本章了解不同的物体类型。
使用力、速度和扭矩
无论我们创建何种类型的模拟,我们很可能会至少想要控制一个物体。在 Box2D 中移动物体,我们可以施加线性或角力,设置线性或角速度,并以扭矩的形式施加角力。在本食谱中,我们将看到如何对多个物体施加这些力和速度。
准备就绪...
按照本章开始部分Box2D 物理扩展一节的步骤创建一个新活动,以便创建能够对力、速度和扭矩做出反应的物体。然后,更新活动,包括来自代码捆绑包中ForcesVelocitiesTorqueActivity
类的附加代码。
如何操作...
参考补充的ForcesVelocitiesTorqueActivity
类,以获取本食谱的完整示例。我们将在本节中仅介绍食谱的基础知识:
-
我们首先会使用处理物体线性运动的方法。在
LinearForceRect
矩形重写的onAreaTouched()
方法中放置以下代码片段:LinearForceBody.applyForce(0f, 2000f, LinearForceBody.getWorldCenter().x, LinearForceBody.getWorldCenter().y);
-
接下来,将此代码插入到
LinearImpulseRect
矩形的onAreaTouched()
方法中:LinearImpulseBody.applyLinearImpulse(0f, 200f, LinearImpulseBody.getWorldCenter().x, LinearImpulseBody.getWorldCenter().y);
-
然后,将此代码添加到
LinearVelocityRect
矩形的onAreaTouched()
方法中:LinearVelocityBody.setLinearVelocity(0f, 20f);
-
现在,我们将使用影响物体角运动的
Body
方法。将此代码放在AngularTorqueRect
矩形的onAreaTouched()
方法中:AngularTorqueBody.applyTorque(2000f);
-
在
AngularImpulseRect
矩形的onAreaTouched()
方法中插入以下代码:AngularImpulseBody.applyAngularImpulse(20f);
-
最后,将此代码添加到
AngularVelocityRect
矩形的onAreaTouched()
方法中:AngularVelocityBody.setAngularVelocity(10f);
工作原理...
在第一步中,我们通过调用LinearForceBody
的applyForce()
方法,并在 x 轴上使用0f
,在 y 轴上使用2000f
的力参数,在其世界坐标中心LinearForceBody.getWorldCenter().x
和LinearForceBody.getWorldCenter().y
处施加一个强大的、正的垂直力。
第二步通过LinearImpulseBody.applyLinearImpulse()
方法在LinearImpulseBody
物体上应用一个线性冲量。applyLinearImpulse()
方法的前两个参数是相对于世界坐标轴的冲量量。我们使用值0f
和200f
来应用一个指向正上方的适度冲量。applyLinearImpulse()
方法的剩余两个参数是冲量在世界坐标中应用到的物体的 x 和 y 位置。我们传递LinearImpulseBody.getWorldCenter().x
和LinearImpulseBody.getWorldCenter().y
,以在LinearImpulseBody
物体的中心应用冲量。
在第三步中,我们通过调用LinearVelocityBody.setLinearVelocity()
方法并传入参数0f
和20f
来设置LinearVelocityBody
的线性速度。参数0f
表示物体在 x 轴上不会移动,而参数20f
则立即将 y 轴上的运动速度设置为每秒 20 米。使用setLinearVelocity()
方法时,速度会自动设置在物体的质心上。
第四步给AngularTorqueBody
应用一个扭矩。我们调用AngularTorqueBody.applyTorque()
方法并传入值2000f
,以在物体的质心上给AngularTorqueBody
施加一个非常大的扭矩。
在第五步中,我们通过调用AngularImpulseBody.applyAngularImpulse()
方法并传入值20f
,给AngularImpulseBody
物体应用一个角冲量。这个小的角冲量将被应用到AngularImpulseBody
物体的质心上。
在最后一步中,我们设置AngularVelocityBody
物体的角速度。我们调用AngularVelocityBody.setAngularVelocity()
方法并传入值10f
,使物体立即以每秒 10 弧度的速度旋转。
还有更多...
冲量与力的不同之处在于,它们独立于时间步长起作用。实际上,冲量等于力乘以时间。同样,力等于冲量除以时间。
设置物体的速度和应用冲量相似,但有一个重要的区别——直接应用冲量会增加或减少速度,而设置速度并不会逐渐增加或减少速度。
另请参阅
-
本章中的《Box2D 物理扩展简介》。
-
本章中了解不同的物体类型。
对特定物体应用反重力
在上一个食谱中,我们了解了力如何影响物体。使用与重力相对抗的恒定力,我们可以使物体从物理世界的重力中释放出来。如果与重力相对抗的力足够大,物体甚至会飘走!在这个食谱中,我们将创建一个与重力相抵消的物体。
准备好了...
按照本章开始部分《Box2D 物理扩展简介》一节中的步骤创建一个活动。这个活动将有助于创建一个受到恒定力作用的物体,该力与重力相对抗。
如何操作...
对于此教程,按照以下步骤创建一个反对重力的刚体:
-
在活动中放置以下定义:
Body gravityBody; Body antigravityBody; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(2f, 0.5f, 0.9f);
-
接下来,创建一个矩形和刚体,以演示重力对刚体的正常影响。在
onPopulateScene()
方法中放置以下代码片段:Rectangle GravityRect = new Rectangle(300f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); GravityRect.setColor(0f, 0.7f, 0f); mScene.attachChild(GravityRect); mScene.registerTouchArea(GravityRect); gravityBody = PhysicsFactory.createBoxBody(mPhysicsWorld, GravityRect, BodyType.DynamicBody, boxFixtureDef); gravityBody.setLinearDamping(0.4f); gravityBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( GravityRect, gravityBody));
-
最后,创建一个矩形和刚体,以展示如何通过在每次更新时施加反重力力使刚体忽略重力:
Rectangle AntiGravityRect = new Rectangle(500f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()) { @Override protected void onManagedUpdate(final float pSecondsElapsed) { super.onManagedUpdate(pSecondsElapsed); antigravityBody.applyForce( -mPhysicsWorld.getGravity().x * antigravityBody.getMass(), -mPhysicsWorld.getGravity().y * antigravityBody.getMass(), antigravityBody.getWorldCenter().x, antigravityBody.getWorldCenter().y); } }; AntiGravityRect.setColor(0f, 0f, 0.7f); mScene.attachChild(AntiGravityRect); mScene.registerTouchArea(AntiGravityRect); antigravityBody = PhysicsFactory.createBoxBody(mPhysicsWorld, AntiGravityRect, BodyType.DynamicBody, boxFixtureDef); antigravityBody.setLinearDamping(0.4f); antigravityBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( AntiGravityRect, antigravityBody));
它是如何工作的...
我们采取的第一步是定义一个受重力影响的刚体、一个反对重力的刚体,以及创建刚体时使用的夹具定义。
接下来,我们创建一个受重力影响的矩形及其对应的刚体。有关创建矩形的更多信息,请参阅第二章中的将基元应用于层教程,使用实体,或有关创建刚体的更多信息,请参阅本章中的了解不同的刚体类型教程。
然后,我们创建反重力刚体及其连接的矩形。通过重写反重力矩形的onManagedUpdate()
方法,我们可以在其中放置代码,这些代码将在每次引擎更新后运行。在AntiGravityRect
矩形的情况下,我们用antigravityBody.applyForce()
方法填充onManagedUpdate()
方法,传递负的mPhysicsWorld.getGravity()
方法的x
和y
值乘以antigravityBody
的质量,并最终设置力在世界中心的antigravityBody
上施加。通过在onManagedUpdate()
方法中使用与物理世界的重力完全相反的力,每次更新后,反重力刚体都能对抗物理世界的重力。此外,我们施加的力必须乘以刚体的质量,以完全抵消重力的效果。参考以下图表以更好地了解反重力刚体的功能:
另请参阅
-
本章中的 Box2D 物理扩展介绍。
-
在本章中使用力、速度和扭矩。
使用关节
在 Box2d 中,关节用于连接两个刚体,使每个刚体以某种方式附着在另一个上。各种类型的关节使我们能够定制角色、车辆和世界。此外,关节可以在模拟过程中创建和销毁,这为我们的游戏提供了无限的可能性。在本教程中,我们将创建一个线关节,以演示如何在物理世界中设置和使用关节。
准备工作...
按照本章开始部分给出的Box2D 物理扩展介绍部分的步骤创建一个活动。这个活动将有助于创建两个刚体和一个连接线关节,我们将在此教程中使用它们。参考补充代码中的JointsActivity
类,了解更多类型的关节示例。
如何操作...
按照以下步骤创建一个线关节:
-
在我们的活动中定义以下变量:
Body LineJointBodyA; Body LineJointBodyB; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.2f, 0.9f);
-
在
onPopulateScene()
方法中添加以下代码,以创建两个矩形及其相关联的物体:Rectangle LineJointRectA = new Rectangle(228f, 240f, 30f, 30f, this.getEngine().getVertexBufferObjectManager()); LineJointRectA.setColor(0.5f, 0.25f, 0f); mScene.attachChild(LineJointRectA); LineJointBodyA = PhysicsFactory.createBoxBody(mPhysicsWorld, LineJointRectA, BodyType.KinematicBody, boxFixtureDef); Rectangle LineJointRectB = new Rectangle(228f, 200f, 30f, 30f, this.getEngine().getVertexBufferObjectManager()) { @Override protected void onManagedUpdate(final float pSecondsElapsed) { super.onManagedUpdate(pSecondsElapsed); LineJointBodyB.applyTorque(1000f); LineJointBodyB.setAngularVelocity( Math.min( LineJointBodyB.getAngularVelocity(),0.2f)); } }; LineJointRectB.setColor(0.75f, 0.375f, 0f); mScene.attachChild(LineJointRectB); LineJointBodyB = PhysicsFactory.createBoxBody(mPhysicsWorld, LineJointRectB, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( LineJointRectB, LineJointBodyB));
-
在前一步显示的代码之后放置以下代码,以创建一个连接前一步物体的线关节:
final LineJointDef lineJointDef = new LineJointDef(); lineJointDef.initialize(LineJointBodyA, LineJointBodyB, LineJointBodyB.getWorldCenter(), new Vector2(0f,1f)); lineJointDef.collideConnected = true; lineJointDef.enableLimit = true; lineJointDef.lowerTranslation = -220f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; lineJointDef.upperTranslation = 0f; lineJointDef.enableMotor = true; lineJointDef.motorSpeed = -200f; lineJointDef.maxMotorForce = 420f; mPhysicsWorld.createJoint(lineJointDef);
工作原理...
我们首先定义两个物体LineJointBodyA
和LineJointBodyB
,它们将连接到我们的线关节,以及将应用于这些物体的boxFixtureDef
夹具定义。关于创建夹具定义的更多信息,请参考本章开始部分提供的Box2D 物理扩展介绍食谱。
在第二步中,我们使用Rectangle()
构造函数创建LineJointRectA
矩形,其位置为228f
和240f
,使其位于场景左半部分的中间位置,高度和宽度设置为30f
以使其成为一个小正方形。然后,我们通过调用LineJointRectA.setColor()
方法并传入参数0.5f
、0.25f
和0f
将其颜色设置为深橙色。接下来,我们通过调用PhysicsFactory.createBoxBody()
构造函数创建与LineJointRectA
矩形相关联的LineJointBodyA
物体,传入参数mPhysicsWorld
(即我们的物理世界)、LineJointRectA
(用于定义物体的形状和位置)、BodyType
为BodyType.KinematicBody
以及boxFixtureDef
夹具定义。
接下来,我们以创建LineJointRectA
和LineJointBodyA
相同的方式处理LineJointRectB
和LineJointBodyB
的创建,但在创建LineJointRectB
时增加了重写的onManagedUpdate()
方法,并添加了一个PhysicsConnector
类以连接LineJointRectB
和LineJointBodyB
。LineJointRectB
的onManagedUpdate()
方法通过调用LineJointBodyB.applyTorque()
方法并传入值1000f
,对LineJointBodyB
施加大的扭矩。施加扭矩后,我们确保LineJointBodyB
物体的角速度不超过0.2f
,通过将Math.min(LineJointBodyB.getAngularVelocity(), 0.2f)
传递给LineJointBodyB.setAngularVelocity()
方法。最后,在第二步末尾创建并注册的PhysicsConnector
类将我们场景中的LineJointRectB
与物理世界中的LineJointBodyB
连接起来。
在第三步中,我们创建线性关节。为了初始化线性关节,我们使用lineJointDef.initialize()
方法,并传入相关联的刚体LineJointBodyA
和LineJointBodyB
。然后,我们将LineJointBodyB
的世界中心作为关节的锚点,并传入包含关节的世界单位轴的Vector2
。我们关节的世界轴设置为0f
和1f
,这意味着在 x 轴上没有移动,在 y 轴上以1f
的比例移动。然后,我们通过将lineJointDef.collideConnected
变量设置为true
来告诉关节允许两个刚体之间的碰撞,并通过将lineJointDef.enableLimit
变量设置为true
来启用关节的限制,这限制了LineJointBodyB
与第一个刚体的距离。为了设置关节的下限距离,即LineJointBodyB
可以在负方向上移动多远,我们将lineJointDef.lowerTranslation
变量设置为-220f / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
。对于上限距离,我们将lineJointDef.upperTranslation
变量设置为0f
,以防止LineJointBodyB
被推到LineJointBodyA
之上。接下来,我们通过将lineJointDef.enableMotor
变量设置为true
来启用关节的电机,这将根据电机的速度将LineJointBodyB
向LineJointBodyA
拉或推。最后,我们通过将lineJointDef.motorSpeed
变量设置为-200f
来给关节的电机一个快速的负速度,使LineJointBodyB
向lowerTranslation
限制移动,并通过将lineJointDef.maxMotorForce
变量设置为420f
来给电机一个强大的最大力。
线性关节的作用类似于汽车的悬挂和车轮部分。它允许在一个轴上进行约束运动,通常是车辆的垂直方向,并允许第二个刚体旋转或必要时作为动力轮。下图说明了线性关节的各个组成部分:
还有更多...
所有关节都有两个刚体,并为我们提供了允许连接刚体之间碰撞的选项。我们可以在需要时启用碰撞,但每个关节的collideConnected
变量的默认值是false
。此外,所有关节的第二个刚体应该是具有BodyType.DynamicBody
类型的BodyType
。
对于具有频率的任何关节,该频率决定了关节的弹性行为,切勿将频率设置为超过物理世界时间步长的一半。如果物理世界的时间步长为 40,我们应分配给关节频率的最大值应为20f
。
如果在关节处于活动状态时,与关节连接的任一刚体被销毁,则关节也会被销毁。这意味着当我们处理物理世界时,只要我们销毁所有刚体,就不需要销毁其中的关节。
更多关节类型
线性关节只是我们可以在物理模拟中使用的几种关节类型之一。其他类型的关节包括距离关节、鼠标关节、棱柱关节、滑轮关节、旋转关节和焊接关节。继续阅读以了解每种类型的更多信息。参考补充的JointsActivity
类,以获得每种关节类型的更深入示例。
距离关节
距离关节只是试图保持其连接的刚体之间的特定距离。如果我们不设置距离关节的长度,它会假定长度为其刚体之间的初始距离。以下代码创建了一个距离关节:
final DistanceJointDef distanceJointDef = new DistanceJointDef();
distanceJointDef.initialize(DistanceJointBodyA,
DistanceJointBodyB, DistanceJointBodyA.getWorldCenter(),
DistanceJointBodyB.getWorldCenter());
distanceJointDef.length = 3.0f;
distanceJointDef.frequencyHz = 1f;
distanceJointDef.dampingRatio = 0.001f;
请注意,我们通过传递两个要连接的刚体DistanceJointBodyA
和DistanceJointBodyB
以及刚体的中心点DistanceJointBodyA.getWorldCenter()
和DistanceJointBodyB.getWorldCenter()
作为关节的锚点来初始化距离关节。接下来,我们通过设置distanceJointDef.length
变量为3.0f
来设置关节的长度,这告诉关节在物理世界中两个刚体应该相隔 3 米。最后,我们将distanceJointDef.frequencyHz
变量设置为1f
以强制关节弹簧具有小的频率,并将distanceJointDef.dampingRatio
变量设置为0.001f
以产生连接刚体的非常小的阻尼效果。为了更容易理解距离关节的外观,请参考前面的图表。
鼠标关节
鼠标关节试图使用设定的最大力将一个刚体拉到特定位置,通常是触摸的位置。它是一个很好的测试用关节,但对于大多数游戏的发布版本,我们应选择使用适当的代码将动力刚体移动到触摸注册的位置。要了解鼠标关节的作用,请参考前面的图表。以下代码定义了一个鼠标关节:
final MouseJointDef mouseJointDef = new MouseJointDef();
mouseJointDef.bodyA = MouseJointBodyA;
mouseJointDef.bodyB = MouseJointBodyB;
mouseJointDef.dampingRatio = 0.0f;
mouseJointDef.frequencyHz = 1f;
mouseJointDef.maxForce = (100.0f * MouseJointBodyB.getMass());
与其他关节不同,鼠标关节没有initialize()
方法来帮助设置关节。我们首先创建mouseJointDef
鼠标关节定义,并将mouseJointDef.bodyA
变量设置为MouseJointBodyA
,将mouseJointDef.bodyB
变量设置为MouseJointBodyB
,以告诉关节它将连接哪些刚体。在我们所有的模拟中,MouseJointBodyA
应该是一个不动的刚体,在鼠标关节激活时不会移动。
接下来,我们将mouseJointDef.dampingRatio
变量设置为0.0f
,使关节完全没有阻尼。然后,我们将mouseJointDef.frequencyHz
变量设置为1f
,以在MouseJointBodyB
达到鼠标关节的目标时强制产生轻微的频率响应,我们可以在下面的代码中看到这一点。最后,我们将mouseJointDef
的maxForce
变量设置为(100.0f * MouseJointBodyB.getMass())
方法。强大的力100.0f
乘以MouseJointBodyB
的质量,以考虑MouseJointBodyB
质量的变化。
在这段代码中,我们初始化了鼠标关节,但它只应在模拟开始后激活。要在模拟运行时从类的onSceneTouchEvent()
方法内部激活鼠标关节,请参阅以下代码。请注意,mouseJoint
变量是一个鼠标关节,在类级别创建:
if(pSceneTouchEvent.isActionDown()) {
mouseJointDef.target.set(MouseJointBodyB.getWorldCenter());
mouseJoint = (MouseJoint)mPhysicsWorld.createJoint(
mouseJointDef);
final Vector2 vec = Vector2Pool.obtain(
pSceneTouchEvent.getX() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,
pSceneTouchEvent.getY() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT);
mouseJoint.setTarget(vec);
Vector2Pool.recycle(vec);
} else if(pSceneTouchEvent.isActionMove()) {
final Vector2 vec = Vector2Pool.obtain(
pSceneTouchEvent.getX() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT,
pSceneTouchEvent.getY() /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT);
mouseJoint.setTarget(vec);
Vector2Pool.recycle(vec);
} else if(pSceneTouchEvent.isActionCancel() ||
pSceneTouchEvent.isActionOutside() ||
pSceneTouchEvent.isActionUp()) {
mPhysicsWorld.destroyJoint(mouseJoint);
}
当屏幕首次被触摸时,通过检查pSceneTouchEvent.isActionDown()
确定,我们使用mouseJointDef.target.set()
方法将初始鼠标关节目标设置为MouseJointBodyB
的世界中心,通过MouseJointBodyB.getWorldCenter()
方法获取。然后,我们通过在物理世界中使用MouseJoint
关节转换的mPhysicsWorld.createJoint()
方法以及mouseJointDef
变量作为参数创建鼠标关节定义,来设置mouseJoint
变量。关节创建后,我们从Vector2Pool
创建Vector2
,保存场景触摸位置pSceneTouchEvent.getX()
和pSceneTouchEvent.getY()
,通过除以PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
转换为物理世界的坐标。
然后,我们将mouseJoint
关节的目标变量更改为先前创建的Vector2
,并将Vector2
回收至Vector2Pool
。当触摸仍然有效时,通过检查pSceneTouchEvent.isActionMove()
确定,我们使用在物理世界中创建鼠标关节后立即使用的相同过程来更新鼠标关节的目标。我们从Vector2Pool
获取Vector2
,将其设置为转换后的物理世界触摸位置,将鼠标关节的目标设置为该Vector2
,然后回收Vector2
。一旦触摸释放,通过检查pSceneTouchEvent.isActionCancel()
,pSceneTouchEvent.isActionOutside()
,或pSceneTouchEvent.isActionUp()
确定,我们通过调用mPhysicsWorld.destroyJoint()
方法并传入我们的mouseJoint
变量作为参数,在世界中销毁鼠标关节。
斜轴关节(prismatic joint)
斜轴关节允许其连接的刚体沿单一轴滑动分离或靠拢,必要时可以由电机驱动。刚体具有锁定的旋转,因此在使用斜轴关节设计模拟时我们必须牢记这一点。考虑前面的图表来理解这个关节是如何工作的。以下代码创建了一个斜轴关节:
final PrismaticJointDef prismaticJointDef =
new PrismaticJointDef();
prismaticJointDef.initialize(PrismaticJointBodyA,
PrismaticJointBodyB, PrismaticJointBodyA.getWorldCenter(),
new Vector2(0f,1f));
prismaticJointDef.collideConnected = false;
prismaticJointDef.enableLimit = true;
prismaticJointDef.lowerTranslation = -80f /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT;
prismaticJointDef.upperTranslation = 80f /
PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT;
prismaticJointDef.enableMotor = true;
prismaticJointDef.maxMotorForce = 400f;
prismaticJointDef.motorSpeed = 500f;
mPhysicsWorld.createJoint(prismaticJointDef);
定义了prismaticJointDef
变量之后,我们使用prismaticJointDef.initialize()
方法对其进行初始化,并传递我们的连接刚体PrismaticJointBodyA
和PrismaticJointBodyB
,锚点被声明为PrismaticJointBodyA
在世界坐标系中的中心点,以及关节的世界单位向量轴,以Vector2
对象Vector2(0f,1f)
表示。我们通过将prismaticJointDef.collideConnected
变量设置为false
来禁用两个刚体之间的碰撞,并通过将prismaticJointDef.enableLimit
变量设置为true
来启用关节滑动范围的限制。
为了设置关节的限制,我们将lowerTranslation
和upperTranslation
属性分别设置为-80f
和80f
像素,然后除以PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT
,将像素限制转换为物理世界中的米。最后,我们通过将prismaticJointDef.enableMotor
属性设置为true
来启用马达,通过prismaticJointDef.maxMotorForce
属性将其最大力设置为400f
,并通过prismaticJointDef.motorSpeed
属性将其速度设置为正值500f
,以驱动PrismaticJointBodyB
向关节的上限移动。
滑轮关节
滑轮关节的作用非常类似于真实的滑轮——当一侧下降时,另一侧上升。滑轮关节的长度在初始化时确定,并且在创建后不应更改。参考前图以了解滑轮关节的外观。以下代码创建了一个滑轮关节:
final PulleyJointDef pulleyJointDef = new PulleyJointDef();
pulleyJointDef.initialize(
PulleyJointBodyA,
PulleyJointBodyB,
PulleyJointBodyA.getWorldPoint(
new Vector2(0f, 2.5f)),
PulleyJointBodyB.getWorldPoint(
new Vector2(0f, 2.5f)),
PulleyJointBodyA.getWorldCenter(),
PulleyJointBodyB.getWorldCenter(),
1f);
mPhysicsWorld.createJoint(pulleyJointDef);
创建了pulleyJointDef
变量之后,我们通过pulleyJointDef.initialize()
方法对其进行初始化。pulleyJointDef.initialize()
方法的前两个参数是两个连接的刚体,分别是PulleyJointBodyA
和PulleyJointBodyB
。接下来的两个参数是滑轮的地面锚点,在这种情况下,它们分别位于每个刚体上方2.5f
米处。为了获取每个刚体上方世界坐标系中的相对点,我们使用每个刚体的getWorldPoint()
方法,x 参数为0
,y 参数为每个刚体上方2.5
米。pulleyJointDef.initialize()
方法的第五和第六个参数是每个刚体在世界坐标系中的锚点。在这个模拟中,我们使用中心点,因此传递每个连接刚体的getWorldCenter()
方法。
方法的最后一个参数是滑轮的比例,在这种情况下是1f
。比例为2
将导致PulleyJointBodyA
相对于其地面锚点的移动距离是PulleyJointBodyB
的每段距离变化的两倍。此外,因为PulleyJointBodyA
相对于其地面锚点移动所需的工作量是PulleyJointBodyB
所需工作量的一半,所以PulleyJointBodyA
比PulleyJointBodyB
有更大的杠杆作用,导致在正常模拟中PulleyJointBodyA
更容易受到重力的影响,从而起到提升PulleyJointBodyB
的作用。创建滑轮接头的最后一步是调用mPhysicsWorld.createJoint()
方法,并将我们的pulleyJointDef
变量传递给它。
转动关节
转动关节是 Box2D 模拟中最受欢迎的关节类型。它本质上是两个连接体之间的一个支点,具有可选的电机和限制。查看前一个图表可以帮助更清楚地了解转动关节的工作原理。以下代码创建了一个转动关节:
final RevoluteJointDef revoluteJointDef = new RevoluteJointDef();
revoluteJointDef.initialize(
RevoluteJointBodyA,
RevoluteJointBodyB,
RevoluteJointBodyA.getWorldCenter());
revoluteJointDef.enableMotor = true;
revoluteJointDef.maxMotorTorque = 5000f;
revoluteJointDef.motorSpeed = -1f;
mPhysicsWorld.createJoint(revoluteJointDef);
我们首先将revoluteJointDef
定义为新创建的RevoluteJointDef()
方法。然后,我们使用revoluteJointDef.initialize()
方法初始化它,参数为RevoluteJointBodyA
和RevoluteJointBodyB
以连接两个刚体,以及RevoluteJointBodyA
的getWorldCenter()
方法来定义关节旋转的位置。接着,我们通过将revoluteJointDef.enableMotor
属性设置为true
来启用我们的转动关节的电机。然后,我们将maxMotorTorque
属性设置为5000f
以使电机非常强大,并将motorSpeed
属性设置为-1f
以使电机以非常慢的速度顺时针旋转。最后,我们通过调用mPhysicsWorld.createJoint(revoluteJointDef)
在物理世界中创建转动关节,使物理世界使用我们的revoluteJointDef
变量创建一个转动关节。
焊接接头
焊接关节将两个刚体连接在一起并禁用它们之间的旋转。它对于可破坏物体非常有用,但是较大的可破坏物体可能会由于 Box2D 的迭代位置求解器产生的抖动而偶尔失败。在这种情况下,我们会从多个夹具创建物体,并在物体分离时以新刚体的形式重新创建每个部分。参考前一个焊接关节的图表,可以更好地理解它是如何工作的。以下代码创建了一个焊接关节:
final WeldJointDef weldJointDef = new WeldJointDef();
weldJointDef.initialize(WeldJointBodyA, WeldJointBodyB,
WeldJointBodyA.getWorldCenter());
mPhysicsWorld.createJoint(weldJointDef);
要创建我们的焊接关节,我们首先创建一个名为weldJointDef
的WeldJointDef
定义。然后,通过调用weldJointDef.initialize()
方法并传入WeldJointBodyA
和WeldJointBodyB
的身体参数以及关节在世界坐标系中WeldJointBodyA
身体的中心作为锚点来初始化它。焊接关节的锚点似乎可以放在任何地方,但由于 Box2D 在碰撞处理焊接关节的锚点时的方式,我们希望将其放置在连接身体之一的中心位置。否则,在与具有大质量的身体碰撞时,可能会导致关节剪切或位移。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的身体类型。
创建布娃娃
物理模拟中最受欢迎的角色描绘之一是布娃娃。这类角色的视觉外观根据细节而有所不同,但底层系统始终相同——我们只是通过关节将几个物理身体附着到更大的物理身体上。在本食谱中,我们将创建一个布娃娃。
准备工作...
复习本章中Box2D 物理扩展介绍食谱中物理活动创建,了解不同的身体类型食谱中身体创建,以及处理关节食谱中旋转关节和鼠标关节的使用。
如何操作...
请参考补充的RagdollActivity
类,这是我们在此食谱中使用的代码。
工作原理...
第一步是定义代表布娃娃多个身体的变量。我们的身体包括代表头部的headBody
,代表躯干的torsoBody
,代表左臂的leftUpperarmBody
和leftForearmBody
,代表右臂的rightUpperarmBody
和rightForearmBody
,代表左腿的leftThighBody
和leftCalfBody
,以及最后代表右腿的rightThighBody
和rightCalfBody
。以下图表显示了如何使用旋转关节将我们所有的身体连接在一起:
接下来,我们定义了当屏幕被触摸时用来抛掷布娃娃的鼠标关节所需的变量,即Vector2 localMouseJointTarget
鼠标关节的目标,mouseJointDef
鼠标关节定义,mouseJoint
关节,以及鼠标关节的地面身体MouseJointGround
。然后,我们创建了将应用于布娃娃各个部分的固定装置定义——头部的headFixtureDef
,躯干的torsoFixtureDef
,手臂的armsFixtureDef
以及腿部的legsFixtureDef
。有关创建固定装置定义的更多信息,请参考本章中的Box2D 物理扩展介绍食谱。
然后,在onPopulateScene()
方法中,我们为布娃娃的每个身体部位创建单独的矩形和它们关联的物体,这些在活动中定义。每个矩形与其对应身体部位的位置和大小完全匹配。在我们创建要链接到矩形的物体时,我们通过PhysicsFactory.createBoxBody()
方法的最后一个参数分配活动中定义的适当的固定定义。最后,对于每个矩形身体组,我们向物理世界注册一个PhysicsConnector
对象。有关创建物体和PhysicsConnector
对象的更多信息,请参考本章中的了解不同的身体类型食谱。
接下来,我们将创建许多旋转关节,用以连接布娃娃的身体部位。每个关节的锚点位置是在世界坐标系中我们希望该身体部位旋转的地方,通过每个关节定义的initialize()
方法的最后一个参数传递。我们确保每个关节连接的物体不会相互碰撞,通过将关节的collideConnected
属性设置为false
。这不会阻止物体与其他布娃娃部分发生碰撞,但确实允许关节的物体在旋转时重叠。接下来,注意我们给关节定义应用了限制,以防止身体部位移动超出一定的运动范围,这很像人类移动四肢时的限制。如果不为关节设置限制,将会创建一个允许其四肢完全旋转的布娃娃,这种表示虽然不真实,但对于某些模拟是必要的。有关旋转关节的更多信息,请参考本章中的使用关节食谱。
创建表示布娃娃关节的旋转关节之后,我们创建了mouseJointDef
鼠标关节定义,这将允许我们拖动布娃娃在场景中飞来飞去。我们将布娃娃的headBody
作为鼠标关节的第二个物体,但根据模拟的需要,可以使用连接到布娃娃的任何物体。创建布娃娃的最后一个步骤是设置鼠标关节,以便在运行时通过onSceneTouchEvent()
方法的触摸交互使用。有关使用鼠标关节的更多信息,请参考本章中的使用关节食谱。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的身体类型。
-
本章中的使用关节。
创建一根绳子
尽管使用 Box2D 模拟真实的绳子是性能密集型的,但一个简单的绳子不仅快速,而且非常可定制。从构建的角度来看,绳子类似于布娃娃,可以为游戏增加额外的可玩层次。如果一个物理模拟看起来过于平淡,无法吸引玩家,那么添加绳子将肯定给玩家另一个喜欢游戏的原因。在本食谱中,我们将创建一个用于我们模拟的物理启用的绳子。
准备好...
回顾本章中的Box2D 物理扩展介绍食谱中基于物理的活动创建,了解不同的身体类型食谱中的身体创建,以及使用关节食谱中的旋转关节和鼠标关节的使用。
如何操作...
请参考补充的Rope
和RopeActivity
类,了解我们在此食谱中使用的代码。
它是如何工作的...
在 Box2D 中创建的绳子可以被视为由关节连接在一起的相似身体的链条。我们可以使用矩形或圆形身体来定义绳子的每个部分,但圆形身体在与其他身体碰撞时抓住并拉伸的可能性较小。查看以下图表,了解我们如何为物理模拟设计绳子:
首先,参考Rope
类,这将使我们更容易创建多条绳子,并一次性为我们的模拟调整所有绳子。Rope
类中的初始代码是一组反映每条绳子特定属性的变量。numRopeSegments
变量保存我们的绳子将拥有的段数。ropeSegmentsLength
和ropeSegmentsWidth
变量保存每个绳子段的长度和宽度。接下来,ropeSegmentsOverlap
变量表示每个绳子段与上一个绳子段重叠多少,这可以防止在轻微拉伸时出现间隙。RopeSegments
数组和RopeSegmentsBodies
数组为我们的绳子的每个部分定义矩形和身体。最后,RopeSegmentFixtureDef
固定定义将保存我们将应用于绳子每个部分的固定装置数据。
接下来,我们创建一个名为Rope
的构造函数,以处理绳子的放置、细节、长度、宽度、重量以及绳子的总体创建。然后,我们为上一步创建的变量赋值。注意,RopeSegmentFixtureDef
固定定义从最大密度开始。由于绳子的每个部分都是通过构造函数后面的for
循环创建的,因此固定装置的密度(从而质量)会递减到最小密度。这通过给最高身体部分最大的强度来防止拉伸,以保持较低身体部分。
在Rope
构造函数的for
循环开始处,我们为每个绳索段定义了旋转关节。关于旋转关节的更多信息,请参见本章中的使用关节部分。然后,我们创建了表示该段的矩形RopeSegments[i]
,并检查确保当i
小于1
时,第一个段根据构造函数中传递的pAttachTo
铰链放置,而其余的段相对于它们的前一段RopeSegments[i-1]
放置。创建矩形时包括了一个重叠值ropeSegmentsOverlap
,以消除由 Box2D 的迭代过程造成的绳索中的间隔。
在我们创建了段的矩形并通过调用RopeSegments[i].setColor(0.97f, 0.75f, 0.54f)
将其颜色设置为棕色之后,我们对RopeSegmentFixtureDef
固定定义应用了密度计算,并使用PhysicsFactory.createCircleBody()
方法基于段的矩形创建了一个圆形体。关于创建体的更多信息,请参考本章中的了解不同的体类型部分。然后,我们通过setAngularDamping(4f)
方法为每个绳索段体设置适中的角阻尼,并通过setLinearDamping(0.5f)
方法设置轻微的线性阻尼,以消除绳索行为中的不可预测性。
之后,我们通过将RopeSegmentsBodies[i].setBullet
属性设置为true
,使绳索段能够作为子弹行动,这减少了我们的段穿过碰撞体的机会。最后,我们为当前绳索段相对于前一段或如果当前段是绳索中的第一段则相对于铰链创建旋转关节。关于旋转关节的更多信息,请参见本章中的使用关节部分。
对于我们的活动类,我们首先创建了用于鼠标关节的必要变量,该关节将绳索的铰链体移动到触摸位置,并定义了我们的RopeHingeBody
体,该体将作为绳索的锚点。然后在onPopulateScene()
方法中,我们创建了RopeHingeBody
体,随后是我们的rope
对象,将绳索铰链体作为第一个参数传递给Rope
构造函数。关于创建体的更多信息,请参考本章中的了解不同的体类型部分。Rope
构造函数的下一个参数告诉我们的绳索要有10
个段长,每个段长25f
像素,宽10f
像素,重叠2f
像素,具有最小密度5f
和最大密度50f
,以及我们附加绳索段矩形的mScene
场景。Rope
构造函数的最后两个参数告诉绳索在我们的mPhysicsWorld
物理世界中创建段体,并将每个段的矩形设置为活动类的VertexBufferObjectManager
管理。
接下来,我们定义并设置用于我们的鼠标关节的变量。请注意,我们将RopeHingeBody
设置为鼠标关节的第二个物体。最后,我们设置onSceneTouchEvent()
方法来处理我们的鼠标关节。有关鼠标关节的更多信息,请参考本章中的处理关节部分。
另请参阅
-
本章中的Box2D 物理扩展介绍。
-
本章中的了解不同的物体类型。
-
本章中的处理关节。
处理碰撞
在基于物理模拟的游戏中,使物体之间的碰撞产生某种效果,无论是播放声音还是处理物体,通常都是必要的部分。处理碰撞一开始看起来可能是一项艰巨的任务,但当我们了解了ContactListener
接口的每个部分如何工作之后,它就会变得很自然。在本教程中,我们将演示如何处理固定装置之间的碰撞。
准备好了...
按照本章开始部分Box2D 物理扩展介绍中的步骤创建一个新活动,这将有助于创建我们的模拟,其中我们将控制碰撞行为。
如何操作...
按照以下步骤演示我们对碰撞的控制:
-
在活动类的开始处放置以下定义:
public Rectangle dynamicRect; public Rectangle staticRect; public Body dynamicBody; public Body staticBody; public boolean setFullAlphaForDynamicBody = false; public boolean setHalfAlphaForDynamicBody = false; public boolean setFullAlphaForStaticBody = false; public boolean setHalfAlphaForStaticBody = false; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(2f, 0f, 0.9f);
-
要在
ContactListener
接口中确定特定物体是否被接触,请在活动中插入以下方法:public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; }
-
下面的方法与之前的方法相似,但除了第一个物体外,还测试了另一个物体。在之前的 方法后,将以下代码添加到类中:
public boolean areBodiesContacted(Body pBody1, Body pBody2, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody1) || pContact.getFixtureB().getBody().equals(pBody1)) if(pContact.getFixtureA().getBody().equals(pBody2) || pContact.getFixtureB().getBody().equals(pBody2)) return true; return false; }
-
接下来,我们将创建一个动态物体和一个静态物体来测试碰撞。在
onPopulateScene()
方法中放置以下内容:dynamicRect = new Rectangle(300f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); dynamicRect.setColor(0f, 0.7f, 0f); dynamicRect.setAlpha(0.5f); mScene.attachChild(dynamicRect); dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRect, BodyType.DynamicBody, boxFixtureDef); dynamicBody.setLinearDamping(0.4f); dynamicBody.setAngularDamping(0.6f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRect, dynamicBody)); staticRect = new Rectangle(500f, 240f, 100f, 100f, this.getEngine().getVertexBufferObjectManager()); staticRect.setColor(0f, 0f, 0.7f); staticRect.setAlpha(0.5f); mScene.attachChild(staticRect); staticBody = PhysicsFactory.createBoxBody(mPhysicsWorld, staticRect, BodyType.StaticBody, boxFixtureDef);
-
现在,我们需要设置物理世界的
ContactListener
属性。在onPopulateScene()
方法中添加以下内容:mPhysicsWorld.setContactListener(new ContactListener(){ @Override public void beginContact(Contact contact) { if(contact.isTouching()) if(areBodiesContacted(staticBody,dynamicBody,contact)) setFullAlphaForStaticBody = true; if(isBodyContacted(dynamicBody,contact)) setFullAlphaForDynamicBody = true; } @Override public void endContact(Contact contact) { if(areBodiesContacted(staticBody,dynamicBody,contact)) setHalfAlphaForStaticBody = true; if(isBodyContacted(dynamicBody,contact)) setHalfAlphaForDynamicBody = true; } @Override public void preSolve(Contact contact, Manifold oldManifold) {} @Override public void postSolve(Contact contact, ContactImpulse impulse) {} });
-
由于物理世界可能会每次接触多次调用
ContactListener
接口,我们希望将所有逻辑从ContactListener
接口移动到一个每次引擎更新只调用一次的更新处理程序中。在onPopulateScene()
方法中放置以下内容,以完成我们的活动:mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(setFullAlphaForDynamicBody) { dynamicRect.setAlpha(1f); setFullAlphaForDynamicBody = false; } else if(setHalfAlphaForDynamicBody) { dynamicRect.setAlpha(0.5f); setHalfAlphaForDynamicBody = false; } if(setFullAlphaForStaticBody) { staticRect.setAlpha(1f); setFullAlphaForStaticBody = false; } else if(setHalfAlphaForStaticBody) { staticRect.setAlpha(0.5f); setHalfAlphaForStaticBody = false; } } @Override public void reset() {} });
它是如何工作的...
首先,我们定义了用于可视化碰撞的矩形和物体。我们还定义了几个布尔变量,这些变量将根据ContactListener
接口的结果进行更改。最后的变量是用于创建具有碰撞功能的物体的固定装置定义。
在第二步和第三步中,我们创建了两个便捷方法isBodyContacted()
和areBodiesContacted()
,这将使在ContactListener
接口中确定物体的存在变得更加容易。注意,每个方法中的if
语句检查了每个物体与两个固定装置的碰撞情况。由于接触监听器传递Contact
对象的方式,我们无法确定哪个固定装置将与特定物体相关联,因此我们必须检查两者。
第四步创建了本模拟中使用的矩形和物体——一个静态的和一个动态的。我们使用它们的setAlpha()
方法将矩形的透明度设置为0.5f
,以演示当前没有发生接触。在碰撞时,矩形的透明度恢复为不透明,并在碰撞结束后重新设置为透明。
在第五步中,我们通过重写继承的方法来设置物理世界的接触监听器。第一个方法,beginContact()
,在物理世界中发生碰撞时被调用。在这个方法中,我们首先通过检查contact
参数的isTouching()
属性来测试碰撞是否真正涉及到两个物体的接触。Box2D 认为,当两个物体的AABB(边界框)重叠时,碰撞就开始了,而不是实际物体接触时。参考下一张图来了解碰撞和接触的区别。之后,我们会检查我们的物体是否都参与了碰撞,或者只有其中一个。如果是,我们将我们的完全不透明布尔变量设置为true
。下一个方法,endContact()
,在物体不再碰撞时被调用。如果我们的物体参与了正在结束的碰撞,我们会将半透明布尔变量设置为true
。接触监听器中的其余方法在碰撞纠正计算发生之前或之后被调用。因为我们只想测试哪些物体发生了碰撞,所以不需要使用这两个方法。
在第六步中,我们创建了一个更新处理器,以从ContactListener
接口中移除有效代码。它只是检查ContactListener
接口内设置的布尔值,以确定在每次引擎更新后需要采取哪些操作。在采取了正确的操作后,我们重置布尔变量。我们需要从接触监听器中移除有效代码的原因是,接触监听器可能会被多次调用,而且通常在每次碰撞时都会被调用多次。如果在接触监听器内部改变游戏的得分,得分通常会比我们预期的变化大得多。我们可以有一个变量来检查是否已经处理了接触,但这样的代码流程会变得混乱,最终会适得其反。
另请参阅
-
本章节的Box2D 物理扩展介绍。
-
本章节讲述理解不同的身体类型。
-
本章节介绍
preSolve
和postSolve
。
使用preSolve
和postSolve
在接触监听器的presolve
方法中使用碰撞的可用数据,该方法在 Box2D 迭代器引起反应之前被调用,这让我们能独特地控制碰撞发生的方式。preSolve()
方法通常用于创建角色可以从下面跳过但仍然可以从上面走过去的“单向”平台。在反应已经启动后被调用的postSolve()
方法,为我们提供了碰撞的纠正数据,也称为冲击力。这些数据可以用来销毁或分解物体。在本教程中,我们将演示如何正确使用ContactListener
接口的preSolve()
和postSolve()
方法。
准备就绪...
按照本章开始部分提供的Box2D 物理扩展介绍部分中的步骤创建一个新活动。这个新活动将方便我们使用接触监听器内调用的preSolve()
和postSolve()
方法。
如何操作...
按以下步骤完成演示这些方法使用的活动:
-
在活动的开始处放置以下定义:
Body dynamicBody; Body staticBody; FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.5f, 0.9f); Vector2 localMouseJointTarget = new Vector2(); MouseJointDef mouseJointDef; MouseJoint mouseJoint; Body groundBody;
-
为了确定哪个或哪些刚体被接触,将这些方法插入到类中:
public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; } public boolean areBodiesContacted(Body pBody1, Body pBody2, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody1) || pContact.getFixtureB().getBody().equals(pBody1)) if(pContact.getFixtureA().getBody().equals(pBody2) || pContact.getFixtureB().getBody().equals(pBody2)) return true; return false; }
-
我们将测试一个小型动态刚体和一个大型静态刚体之间的碰撞。在
onPopulateScene()
方法中放置以下代码以创建这样的刚体:Rectangle dynamicRect = new Rectangle(400f, 60f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); dynamicRect.setColor(0f, 0.6f, 0f); mScene.attachChild(dynamicRect); dynamicBody = PhysicsFactory.createBoxBody(mPhysicsWorld, dynamicRect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( dynamicRect, dynamicBody)); Rectangle staticRect = new Rectangle(400f, 240f, 200f, 10f, this.getEngine().getVertexBufferObjectManager()); staticRect.setColor(0f, 0f, 0f); mScene.attachChild(staticRect); staticBody = PhysicsFactory.createBoxBody(mPhysicsWorld, staticRect, BodyType.StaticBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( staticRect, staticBody));
-
接下来,我们需要为物理世界设置接触监听器。在
onPopulateScene()
方法中插入以下内容:mPhysicsWorld.setContactListener(new ContactListener(){ float maxImpulse; @Override public void beginContact(Contact contact) {} @Override public void endContact(Contact contact) {} @Override public void preSolve(Contact contact, Manifold oldManifold) { if(areBodiesContacted(dynamicBody, staticBody, contact)) if(dynamicBody.getWorldCenter().y < staticBody.getWorldCenter().y) contact.setEnabled(false); } @Override public void postSolve(Contact contact, ContactImpulse impulse) { if(areBodiesContacted(dynamicBody, staticBody, contact)) { maxImpulse = impulse.getNormalImpulses()[0]; for(int i = 1; i < impulse.getNormalImpulses().length; i++) maxImpulse = Math.max( impulse.getNormalImpulses()[i], maxImpulse); if(maxImpulse>400f) dynamicBody.setAngularVelocity(30f); } } });
-
我们希望可以通过触摸想要移动到的位置来移动较小的刚体。添加以下代码以设置一个鼠标关节,使我们能够这样做:
groundBody = mPhysicsWorld.createBody(new BodyDef()); mouseJointDef = new MouseJointDef(); mouseJointDef.bodyA = groundBody; mouseJointDef.bodyB = dynamicBody; mouseJointDef.dampingRatio = 0.5f; mouseJointDef.frequencyHz = 1f; mouseJointDef.maxForce = (40.0f * dynamicBody.getMass()); mouseJointDef.collideConnected = false;
-
最后,在
onSceneTouchEvent()
方法中插入以下内容,以控制上一步创建的鼠标关节:if(pSceneTouchEvent.isActionDown()) { mouseJointDef.target.set(dynamicBody.getWorldCenter()); mouseJoint = (MouseJoint)mPhysicsWorld.createJoint(mouseJointDef); final Vector2 vec = Vector2Pool.obtain(pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); mouseJoint.setTarget(vec); Vector2Pool.recycle(vec); } else if(pSceneTouchEvent.isActionMove()) { final Vector2 vec = Vector2Pool.obtain(pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT); mouseJoint.setTarget(vec); Vector2Pool.recycle(vec); return true; } else if(pSceneTouchEvent.isActionCancel() || pSceneTouchEvent.isActionOutside() || pSceneTouchEvent.isActionUp()) { mPhysicsWorld.destroyJoint(mouseJoint); }
它是如何工作的...
我们首先定义一个静态刚体、一个动态刚体以及一个将用于创建这两个刚体的夹具定义。然后,我们创建两个使使用接触监听器管理碰撞变得更容易的方法。接下来,我们使用相关联的矩形创建刚体。
在第四步中,我们设置了物理世界的接触监听器。注意,我们在接触监听器开始处创建了一个变量maxImpulse
,以在接触监听器的末尾的postSolve()
方法中使用。对于这个模拟,我们不需要beginContact()
和endContact()
方法,因此我们让它们保持为空。在preSolve()
方法中,我们首先测试以确定接触是否发生在我们的两个身体之间,dynamicBody
和staticBody
。如果是,我们测试dynamicBody
是否在我们的staticBody
下方,通过检查dynamicBody.getWorldCenter().y
属性是否小于staticBody.getWorldCenter().y
属性,如果是,我们取消碰撞。这使得动态身体可以从下方穿过静态身体,同时仍然从上方与静态身体发生碰撞。
在postSolve()
方法中,我们测试以确保只处理我们先前定义的动态和静态身体。如果是这样,我们将maxImpulse
变量设置为impulse.getNormalImpulses()
数组中的第一个冲量。这个列表保存了两个碰撞夹具之间所有接触点的纠正冲量。接下来,我们遍历冲量列表,并将maxImpulse
变量设置为当前maxImpulse
值或列表中的当前冲量值,以较大者为准。这为我们提供了碰撞中的最大纠正冲量,然后我们使用它来旋转动态身体,如果冲力足够大,在这个模拟中是400f
的冲量。
第五步初始化用于在屏幕上拖动动态身体的鼠标关节,第六步使用onSceneTouchEvent()
方法控制鼠标关节。有关鼠标关节的更多信息,请参考处理关节。
另请参阅
-
本章节中介绍了Box2D 物理扩展入门。
-
本章节中了解不同的身体类型。
-
本章节中处理关节。
-
本章节中处理碰撞。
创建可破坏的物体
使用物理世界接触监听器中的postSolve()
方法提供的冲量数据,我们可以得到每次碰撞的冲击力。将这个数据扩展到使多体物体破碎,只需确定哪个身体发生碰撞,以及冲击力是否足够大以至于能将身体从多体物体中分离。在本教程中,我们将演示如何创建由多个身体组成的可破坏物体。
准备就绪...
按照本章开始部分Box2D 物理扩展入门一节中的步骤创建一个活动。这个活动将促进我们在此节中使用的可破坏身体组的创建。
如何操作...
按照以下步骤创建一个在受到大力碰撞时可以破碎的物体:
-
向活动类中添加以下定义:
public Body box1Body; public Body box2Body; public Body box3Body; public boolean breakOffBox1 = false; public boolean breakOffBox2 = false; public boolean breakOffBox3 = false; public Joint box1And2Joint; public Joint box2And3Joint; public Joint box3And1Joint; public boolean box1And2JointActive = true; public boolean box2And3JointActive = true; public boolean box3And1JointActive = true; public final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(20f, 0.0f, 0.9f);
-
为了更容易确定哪个身体被接触,请在类中插入此方法:
public boolean isBodyContacted(Body pBody, Contact pContact) { if(pContact.getFixtureA().getBody().equals(pBody) || pContact.getFixtureB().getBody().equals(pBody)) return true; return false; }
-
我们将要创建一个由三个盒子组成的物理对象,这些盒子通过焊接关节保持在一起。在
onPopulateScene()
方法中定义以下盒子:Rectangle box1Rect = new Rectangle(400f, 260f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box1Rect.setColor(0.75f, 0f, 0f); mScene.attachChild(box1Rect); box1Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box1Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box1Rect, box1Body)); Rectangle box2Rect = new Rectangle(380f, 220f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box2Rect.setColor(0f, 0.75f, 0f); mScene.attachChild(box2Rect); box2Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box2Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box2Rect, box2Body)); Rectangle box3Rect = new Rectangle(420f, 220f, 40f, 40f, this.getEngine().getVertexBufferObjectManager()); box3Rect.setColor(0f, 0f, 0.75f); mScene.attachChild(box3Rect); box3Body = PhysicsFactory.createBoxBody(mPhysicsWorld, box3Rect, BodyType.DynamicBody, boxFixtureDef); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector( box3Rect, box3Body));
-
接下来,在上一步中定义的盒子定义之后,在
onPopulateScene()
方法中放置以下焊接关节定义:final WeldJointDef box1and2JointDef = new WeldJointDef(); box1and2JointDef.initialize(box1Body, box2Body, box1Body.getWorldCenter()); box1And2Joint = mPhysicsWorld.createJoint(box1and2JointDef); final WeldJointDef box2and3JointDef = new WeldJointDef(); box2and3JointDef.initialize(box2Body, box3Body, box2Body.getWorldCenter()); box2And3Joint = mPhysicsWorld.createJoint(box2and3JointDef); final WeldJointDef box3and1JointDef = new WeldJointDef(); box3and1JointDef.initialize(box3Body, box1Body, box3Body.getWorldCenter()); box3And1Joint = mPhysicsWorld.createJoint(box3and1JointDef);
-
现在我们需要设置物理世界的接触监听器。将以下代码添加到
onPopulateScene()
方法中:mPhysicsWorld.setContactListener(new ContactListener(){ float maxImpulse; @Override public void beginContact(Contact contact) {} @Override public void endContact(Contact contact) {} @Override public void preSolve(Contact contact, Manifold oldManifold) {} @Override public void postSolve(Contact contact, ContactImpulse impulse) { maxImpulse = impulse.getNormalImpulses()[0]; for(int i = 1; i < impulse.getNormalImpulses().length; i++) { maxImpulse = Math.max(impulse.getNormalImpulses()[i], maxImpulse); } if(maxImpulse>800f) { if(isBodyContacted(box1Body,contact)) breakOffBox1 = true; else if(isBodyContacted(box2Body,contact)) breakOffBox2 = true; else if(isBodyContacted(box3Body,contact)) breakOffBox3 = true; } } });
-
最后,为了从接触监听器中移除逻辑,请在
onPopulateScene()
方法中放置以下更新处理程序:mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(breakOffBox1) { if(box1And2JointActive) mPhysicsWorld.destroyJoint(box1And2Joint); if(box3And1JointActive) mPhysicsWorld.destroyJoint(box3And1Joint); box1And2JointActive = false; box3And1JointActive = false; breakOffBox1 = false; } if(breakOffBox2) { if(box1And2JointActive) mPhysicsWorld.destroyJoint(box1And2Joint); if(box2And3JointActive) mPhysicsWorld.destroyJoint(box2And3Joint); box1And2JointActive = false; box2And3JointActive = false; breakOffBox1 = false; } if(breakOffBox3) { if(box2And3JointActive) mPhysicsWorld.destroyJoint(box2And3Joint); if(box3And1JointActive) mPhysicsWorld.destroyJoint(box3And1Joint); box2And3JointActive = false; box3And1JointActive = false; breakOffBox1 = false; } } @Override public void reset() {} });
工作原理...
第一步初步定义了三个我们将通过焊接关节连接在一起的身体。接下来,我们定义了三个布尔变量,表示如果有,哪个身体应该从身体组中释放。然后,我们定义了三个保持我们的身体在一起的焊接关节以及表示关节是否存在的相应布尔值。最后,我们定义了一个固定装置定义,我们将根据它创建三个盒状身体。
第二步创建了一个方法,该方法允许我们确定一个特定的身体是否参与了碰撞,这与处理碰撞的教程中看到的内容类似。第三步创建我们的身体,第四步创建连接它们的焊接关节。有关创建身体的更多信息,请参考理解不同的物体类型的教程,或者有关使用关节的更多信息,请参考使用关节的教程。
在第五步中,我们设置了物理世界的接触监听器,只创建了maxImpulse
变量,并只填充了postSolve()
方法。在postSolve()
方法中,我们确定碰撞冲量的力是否足以破坏与一个物体相连的关节。如果是,我们会确定应该从组中分离哪个物体,并为此物体设置相关的布尔值。设置ContactListener
接口后,我们注册了一个更新处理程序,根据哪些物体被标记为需要分离来销毁相应的关节。由于三个物体中的每一个都与另外两个物体相连,因此对于组中的每个物体都有两个关节需要销毁。当我们销毁关节时,我们会将每个被销毁的关节标记为非活动状态,这样我们就不会尝试销毁已经销毁的关节。
另请参阅
-
本章节介绍Box2D 物理扩展的入门。
-
本章节讲解如何理解不同的物体类型。
-
本章节讲解如何使用关节。
-
本章节介绍如何使用
preSolve
和postSolve
。
光线投射
通过物理世界进行光线投射是一种从一个点向另一个点发射假想线,并返回距离、遇到的每个固定装置以及每个被撞击表面的法线向量的计算。光线投射可用于任何从激光和视野锥到确定一个假想子弹击中什么的一切。在本教程中,我们将演示如何在我们的物理世界中执行光线投射。
准备工作...
按照本章开始部分Box2D 物理扩展介绍中的步骤创建一个新活动,以便我们在物理世界中使用光线投射。
如何操作...
按照以下步骤创建一个光线投射演示:
-
在活动的开始处放置以下定义:
Body BoxBody; Line RayCastLine; Line RayCastLineHitNormal; Line RayCastLineHitBounce; float[] RayCastStart = {cameraWidth/2f,15f}; float RayCastAngle = 0f; float RayCastNormalAngle = 0f; float RayCastBounceAngle = 0f; float RaycastBounceLineLength = 200f; final FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(1f, 0.5f, 0.9f);
-
当我们告诉物理世界执行光线投射(raycast)时,它会使用一个提供的
callback
接口,让我们可以利用光线投射收集到的信息。在活动中放置以下RayCastCallback
定义:RayCastCallback rayCastCallBack = new RayCastCallback() { @Override public float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction) { float[] linePos = { point.x * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, point.y * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (point.x + (normal.x)) * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, (point.y + (normal.y)) * PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT}; RayCastLineHitNormal.setPosition( linePos[0],linePos[1], linePos[2],linePos[3]); RayCastLineHitNormal.setVisible(true); RayCastNormalAngle = MathUtils.radToDeg( (float) Math.atan2( linePos[3]-linePos[1], linePos[2]-linePos[0])); RayCastBounceAngle = (2*RayCastNormalAngle)-RayCastAngle; RayCastLineHitBounce.setPosition( linePos[0], linePos[1], (linePos[0] + FloatMath.cos((RayCastBounceAngle + 180f) * MathConstants.DEG_TO_RAD) * RaycastBounceLineLength), (linePos[1] + FloatMath.sin((RayCastBounceAngle + 180f) * MathConstants.DEG_TO_RAD)*RaycastBounceLineLength)); RayCastLineHitBounce.setVisible(true); return 0f; } };
-
为了让光线投射有撞击的对象,我们将在物理世界中创建一个盒子。在
onPopulateScene()
方法中插入以下代码片段:Rectangle Box1 = new Rectangle(400f, 350f, 200f, 200f, this.getEngine().getVertexBufferObjectManager()); Box1.setColor(0.3f, 0.3f, 0.3f); BoxBody = PhysicsFactory.createBoxBody(mPhysicsWorld, Box1, BodyType.StaticBody, boxFixtureDef); BoxBody.setTransform(BoxBody.getWorldCenter(), MathUtils.random(0.349f, 1.222f)); mScene.attachChild(Box1); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(Box1, BoxBody));
-
接下来,我们将定义一个
Line
对象,它表示从光线投射中收集到的一些信息。在onPopulateScene()
方法中添加以下内容:RayCastLine = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLine.setColor(0f, 1f, 0f); RayCastLine.setLineWidth(8f); mScene.attachChild(RayCastLine); RayCastLineHitNormal = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLineHitNormal.setColor(1f, 0f, 0f); RayCastLineHitNormal.setLineWidth(8f); mScene.attachChild(RayCastLineHitNormal); RayCastLineHitBounce = new Line(0f, 0f, 0f, 0f, mEngine.getVertexBufferObjectManager()); RayCastLineHitBounce.setColor(0f, 0f, 1f); RayCastLineHitBounce.setLineWidth(8f); mScene.attachChild(RayCastLineHitBounce);
-
最后,我们希望光线投射在触摸场景的任何地方发生。在
onSceneTouchEvent()
方法中放置以下内容:if(pSceneTouchEvent.isActionMove()||pSceneTouchEvent.isActionDown()){ RayCastAngle = MathUtils.radToDeg((float) Math.atan2(pSceneTouchEvent.getY() - RayCastStart[1], pSceneTouchEvent.getX() - RayCastStart[0])); RayCastLine.setPosition( RayCastStart[0], RayCastStart[1], pSceneTouchEvent.getX(), pSceneTouchEvent.getY()); RayCastLine.setVisible(true); RayCastLineHitNormal.setVisible(false); RayCastLineHitBounce.setVisible(false); mPhysicsWorld.rayCast(rayCastCallBack, new Vector2( RayCastStart[0] / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, RayCastStart[1] / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT), new Vector2( pSceneTouchEvent.getX() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, pSceneTouchEvent.getY() / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT)); } if(pSceneTouchEvent.isActionUp() || pSceneTouchEvent.isActionOutside() || pSceneTouchEvent.isActionCancel()) { RayCastLine.setVisible(false); }
工作原理...
我们首先定义了一个刚体BoxBody
,我们将使用光线投射与之交互。然后,我们定义了几条视觉上表示光线投射的线条。最后,我们定义了一系列变量,帮助我们确定光线投射的位置和结果。
在第二步中,我们定义了一个RayCastCallback
接口,当我们请求物理世界计算光线投射时,将传递这个接口。在回调中,我们使用重写的reportRayFixture()
方法。每次请求的光线投射遇到新的固定装置时,都会调用这个方法。在方法中,我们使用光线投射返回的点和平面变量来修改表示报告固定装置撞击表面的法线位置。设置法线可见后,我们确定法线角度,然后是反弹角度。接着我们定位反弹线以表示光线投射的反弹,并设置反弹线可见。最后,我们返回0
,告诉光线投射在撞击第一个固定装置后终止。为了更好地理解光线投射回调中返回的各种参数,请参考以下图表:
第三步创建了第一步中定义的刚体,并通过调用BoxBody.setTransform()
方法设置了半随机的旋转,最后一个参数为MathUtils.random(0.349f, 1.222f)
,这使得刚体的旋转在0.349
弧度和1.222
弧度之间。第四步创建了表示光线投射各个部分的视觉线条。关于创建刚体的更多信息,请参阅本章中的了解不同的刚体类型菜谱;关于线条的更多信息,请参阅第二章,使用实体。
在第五步中,我们将onSceneTouchEvent()
方法分配给处理射线投射(raycasting)。当触摸发生时,我们首先设置RayCastAngle
变量以供射线投射的回调函数使用。然后,我们定位主射线线,并将其设置为可见,同时将与其他射线相关的线设置为不可见。最后,我们通过传递我们的回调函数、射线投射的起始位置和结束位置,从物理世界中请求射线投射。当触摸事件结束时,我们将主射线线设置为不可见。
另请参阅。
- 本章介绍Box2D 物理扩展。
第七章:使用更新处理器
更新处理器让我们能够每次引擎更新时运行特定的代码片段。一些游戏引擎内置了一个作为主循环的更新处理器,但使用 AndEngine,我们可以轻松地创建尽可能多的这些循环。本章将介绍以下内容:
-
开始使用更新处理器
-
将更新处理器附加到实体
-
使用条件更新处理器
-
处理从游戏中移除实体的操作
-
添加游戏计时器
-
根据经过的时间设置实体属性
开始使用更新处理器
更新处理器本质上是我们在每次引擎更新场景时注册到实体或引擎的代码片段。在大多数情况下,这种更新每次绘制帧时都会发生,无论实体或场景是否已更改。更新处理器可以是运行游戏的一种强大方式,但过度使用它们或在它们中执行繁重的计算将导致性能下降。本节将介绍向活动添加简单更新处理器的基础知识。
准备好了...
创建一个名为UpdateHandlersActivity
的新类,该类继承自BaseGameActivity
。我们将使用这个类来创建一个基本的更新处理器。
如何操作...
按照以下步骤创建一个显示已发生多少次更新的更新处理器:
-
在我们的
UpdateHandlersActivity
类中放置以下定义:public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene;public Font fontDefault32Bold; public Text countingText; public int countingInt = 0;
-
接下来,向类中添加以下重写的方法。
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
最后,将这个最后的方法插入我们的类中:
@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { countingInt++; countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
第一步和第二步涵盖了创建一个简单的BaseGameActivity
类的过程。关于创建BaseGameActivity
类的更多信息,请参见第一章中的Know the life cycle部分,AndEngine 游戏结构。注意,我们在onCreateResources()
方法中创建并加载了一个Font
对象。关于字体和使用它们的Text
实体的更多信息,请参见第二章中的Applying text to a layer部分,使用实体。
在第三步中,我们通过将onCreateResources()
方法中创建的fontDefault32Bold
字体传递给Text
构造函数,以及屏幕居中和最大字符串长度参数10
个字符,创建了一个Text
实体countingText
。将countingText
实体附加到场景后,我们注册了更新处理器。在我们的更新处理器的onUpdate()
方法中,我们增加countingInt
整数,并将countingText
实体的文本设置为该整数。这让我们可以直接在游戏中以文本形式显示已经发生了多少次更新,从而绘制了多少帧。
另请参阅
-
第一章中的Know the life cycle,AndEngine 游戏结构。
-
在第二章,使用实体中Applying text to a layer。
将更新处理器附加到实体上
除了能够将更新处理器与Scene
对象注册之外,我们还可以与特定实体注册更新处理器。通过将更新处理器与实体注册,只有当实体被附加到引擎的场景中时,处理器才会被调用。这个示例通过创建一个更新处理器,它与一个最初未附加的实体注册,来递增屏幕上的文本,演示了这一过程。
准备中...
创建一个名为AttachUpdateHandlerToEntityActivity
的新类,该类继承自BaseGameActivity
并实现IOnSceneTouchListener
接口。我们将使用这个类将更新处理器附加到一个Entity
对象上,等到场景被触摸时再将其附加到场景中。
如何操作...
按照以下步骤创建一个活动,演示更新处理器如何依赖于其父实体来运行:
-
在我们新的活动类中插入以下定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public int countingInt = 0; public Entity blankEntity;
-
然后,在类中放置以下重写的方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
接下来,将以下重写的
onPopulateScene()
方法添加到我们的活动类中:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); blankEntity = new Entity(); blankEntity.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { countingInt++; countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
最后,在我们的
AttachUpdateHandlerToEntityActivity
类中插入以下重写的方法以完成它:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { if(pSceneTouchEvent.isActionDown() && !blankEntity.hasParent()) mScene.attachChild(blankEntity); return true; }
工作原理...
第一步和第二步涵盖了创建一个简单的BaseGameActivity
类。有关创建 BaseGameActivity 类的更多信息,请参见第一章中的Know the life cycle一节,AndEngine 游戏结构。然而请注意,我们在onCreateResources()
方法中创建并加载了一个Font
对象。关于字体和使用它们的Text
实体的更多信息,请参见第二章中的Applying text to a layer一节,使用实体。
第三步创建了一个文本实体countingText
,并将其附加到我们场景的中心。然后,通过调用Entity()
构造函数创建我们的空白实体blankEntity
,并且将更新处理器与其注册。注意,在第四步中的onSceneTouchEvent()
方法检测到触摸事件之前,空白实体并不会被附加到场景中。更新处理器的onUpdate()
方法仅仅是将countingText
实体的文本递增,以显示更新处理器正在运行。
第四步创建了onSceneTouchEvent()
方法,该方法在场景被触摸时被调用。我们检查以确保触摸事件是一个按下动作,并且我们的空白实体还没有父实体,然后将blankEntity
附加到场景中。
还有更多...
运行此示例时,我们可以看到,在空白实体附加到场景之前,更新处理程序不会被调用。这种效果与覆盖实体的onManagedUpdate()
方法类似。将更新处理程序注册到实体可以用于创建具有自身逻辑的敌人,或者是在显示之前不应该动画化的场景部分。注册到Scene
对象中另一个Entity
对象的子Entity
对象的更新处理程序仍然有效。此外,实体的可见性并不影响其注册的更新处理程序是否运行。
另请参阅
-
在本章中开始使用更新处理程序。
-
在第一章中了解AndEngine 游戏结构的生命周期。
-
在第二章中了解AndEngine 实体,使用实体。
-
在第二章中,将文本应用到图层,使用实体。
-
在第二章中覆盖 onManagedUpdate,使用实体。
结合条件使用更新处理程序。
为了减少运行具有繁重计算更新处理程序的性能成本,我们可以包含一个条件语句,告诉更新处理程序在另一个更新周期内运行一组特定的指令。例如,如果我们有敌人检查玩家是否在他们的视线范围内,我们可以选择让视野计算每三次更新只运行一次。在本示例中,我们将演示一个简单的条件语句,通过触摸屏幕在性能密集型计算和非常简单的计算之间进行切换。
准备工作...
创建一个名为UpdateHandlersAndConditionalsActivity
的新类,该类继承自BaseGameActivity
并实现IOnSceneTouchListener
接口。我们将使用这个类来演示如何在使用更新处理程序时使用条件语句。
如何操作...
按照以下步骤创建一个使用条件块来确定要运行哪段代码的更新处理程序:
-
在新类中放置以下定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public int countingInt = 0; public boolean performanceIntensiveLoop = true; public double performanceHeavyVariable;
-
然后,添加以下重写的方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
接下来,插入以下重写的
onPopulateScene()
方法:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "0", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(performanceIntensiveLoop) { countingInt++; for(int i = 3; i < 1000000; i++) performanceHeavyVariable = Math.sqrt(i); } else { countingInt--; } countingText.setText( String.valueOf(countingInt)); } @Override public void reset() {} }); mScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
最后,创建此
onSceneTouchEvent()
方法以完成我们的活动:@Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { if(pSceneTouchEvent.isActionDown()) performanceIntensiveLoop = !performanceIntensiveLoop; return true; }
工作原理...
在第一步中,我们定义了测试床共有的变量以及一个布尔型变量performanceIntensiveLoop
,它告诉我们的更新处理程序采取哪个动作,以及一个双精度变量performanceHeavyVariable
,我们将在性能密集型计算中使用它。第二步为我们的活动创建标准方法。有关创建BaseGameActivity
类的更多信息,请参见第一章中的了解生命周期示例。
在第三步中,我们在注册更新处理器到场景之前创建了countingText
。在每次更新时,它会检查performanceIntensiveLoop
布尔变量,以确定它应该执行繁重任务(几乎调用一百万次Math
类的sqrt()
方法),还是执行简单任务(递减countingInt
变量的文本)。
第四步是onSceneTouchEvent()
方法,每次触摸屏幕时都会切换performanceIntensiveLoop
布尔变量。
另请参阅
-
本章节中的开始使用更新处理器。
-
第一章中的Know the life cycle,AndEngine 游戏结构。
-
第二章中的将文本应用到层上,使用实体。
处理从游戏中移除实体的操作
在更新处理器中分离实体有时可能会抛出IndexOutOfBoundsException
异常,因为实体是在引擎更新过程中被移除的。为了避免该异常,我们创建了一个Runnable
参数,在所有其他更新完成后,在更新线程上最后运行。在本教程中,我们将通过使用BaseGameActivity
类的runOnUpdateThread()
方法,从游戏中安全地移除实体。
准备就绪...
创建一个名为HandlingRemovalOfEntityActivity
的新类,该类继承自BaseGameActivity
。我们将使用这个类来学习如何安全地从更新处理器中移除实体。
如何操作...
按照以下步骤,我们可以在不抛出异常的情况下从一个父实体中移除一个实体:
-
在
HandlingRemovalOfEntityActivity
类中插入以下定义:public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Rectangle spinningRect; public float totalElapsedSeconds = 0f;
-
接下来,将这些重写的方法添加到类中:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
最后,在活动中放置以下
onPopulateScene()
方法以完成它:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { spinningRect = new Rectangle(400f, 240f, 100f, 20f, this.getVertexBufferObjectManager()); spinningRect.setColor(Color.BLACK); spinningRect.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { spinningRect.setRotation( spinningRect.getRotation()+0.4f); totalElapsedSeconds += pSecondsElapsed; if(totalElapsedSeconds > 5f) { runOnUpdateThread(new Runnable() { @Override public void run() { spinningRect.detachSelf(); }}); } } @Override public void reset() {} }); mScene.attachChild(spinningRect); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
在第一步中,我们定义了常规的BaseGameActivity
变量以及一个正方形Rectangle
对象,spinningRect
,它将在原地旋转,还有一个浮点变量totalElapsedSeconds
,用于跟踪自更新处理程序开始以来已经过去了多少秒。第二步创建了标准的BaseGameActivity
方法。有关创建 AndEngine 活动的更多信息,请参见第一章中的Know the life cycle部分,AndEngine 游戏结构。
在第三步中,我们通过调用Rectangle
构造函数并设置屏幕中心位置来创建第一步中定义的spinningRect
矩形。然后通过setColor()
方法将Rectangle
对象设置为黑色。接下来,它注册了我们的更新处理器,记录经过的时间,如果自活动开始以来已经超过5
秒,则从屏幕上移除矩形。请注意,我们从场景中分离矩形的方式是调用runOnUpdateThread()
。此方法将Runnable
参数传递给引擎,以便在更新周期完成后运行。
另请参阅
-
在本章中,开始使用更新处理器。
-
在第一章 AndEngine 游戏结构 中,了解生命周期。
-
在第二章 使用实体 中,给图层应用图元。
添加游戏计时器
许多游戏会倒计时并挑战玩家在给定的时间内完成任务。这样的挑战对玩家来说是有益的,并且常常增加了游戏的重复玩价值。在之前的教程中,我们跟踪了总经过时间。在本教程中,我们将从一个时间开始,并从中减去更新处理器提供的时间。
准备就绪...
创建一个名为 GameTimerActivity
的新类,该类继承自 BaseGameActivity
。我们将使用这个类从更新处理器创建一个游戏计时器。
如何操作...
按照以下步骤使用更新处理器创建游戏计时器:
-
在我们新活动类中放置以下变量定义:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Font fontDefault32Bold; public Text countingText; public float EndingTimer = 10f;
-
接下来,插入以下标准覆盖方法:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { fontDefault32Bold = FontFactory.create( mEngine.getFontManager(), mEngine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.BOLD), 32f, true, Color.BLACK_ARGB_PACKED_INT); fontDefault32Bold.load(); pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
最后,将这个覆盖的
onPopulateScene()
方法添加到GameTimerActivity
类中:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { countingText = new Text(400f, 240f, fontDefault32Bold, "10", 10, this.getVertexBufferObjectManager()); mScene.attachChild(countingText); mScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { EndingTimer-=pSecondsElapsed; if(EndingTimer<=0) { // The timer has ended countingText.setText("0"); mScene.unregisterUpdateHandler(this); } else { countingText.setText(String.valueOf( Math.round(EndingTimer))); } } @Override public void reset() {} }); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
工作原理...
在第一步中,我们将常见的 BaseGameActivity
变量以及一个设置为 10
秒的 EndingTimer
浮点变量定义。第二步为我们的活动创建常见方法。有关创建 BaseGameActivity 类的更多信息,请参见第一章 AndEngine 游戏结构 中的了解生命周期教程。
在第三步中,我们创建 countingText
实体,并使用我们的场景注册一个更新处理器,该处理器通过 pSecondsElapsed
变量倒计时 EndingTimer
变量,直到它达到 0
。当它达到 0
时,我们只需通过调用场景的 unregisterUpdateHandler()
方法,从场景中注销更新处理器。在实际游戏中,计时器结束可能意味着结束一个关卡,甚至召唤下一波敌人攻击玩家。
另请参阅
-
在本章中,开始使用更新处理器。
-
在第一章 AndEngine 游戏结构 中,了解生命周期。
-
在第二章 使用实体 中,给图层应用文本。
根据经过的时间设置实体属性
在移动游戏开发中,设备之间的连贯性是更为重要的方面之一。玩家期望游戏能够适当地缩放以适应他们设备的屏幕,但游戏开发中另一个重要且经常被忽视的方面是基于时间而不是引擎更新来设置移动和动画。在本教程中,我们将使用更新处理器来设置实体的属性。
准备就绪...
创建一个名为 SettingEntityPropertiesBasedOnTimePassedActivity
的新类,该类继承自 BaseGameActivity
。我们将使用这个类来演示如何使用更新处理器来随时间设置实体属性。
如何操作...
按照以下步骤,我们可以根据已经过去的时间设置实体的属性:
-
在活动中定义以下变量:
public static int cameraWidth = 800; public static int cameraHeight = 480; public Scene mScene; public Rectangle spinningRect;
-
然后,在这些重写的方法中放入以下类:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, cameraWidth, cameraHeight)).setWakeLockOptions( WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); }
-
最后,在活动末尾插入此
onPopulateScene()
方法以完成它:@Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { spinningRect = new Rectangle(400f, 240f, 100f, 20f, this.getVertexBufferObjectManager()); spinningRect.setColor(Color.BLACK); spinningRect.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { spinningRect.setRotation( spinningRect.getRotation() + ((pSecondsElapsed*360f)/2f)); } @Override public void reset() {} }); mScene.attachChild(spinningRect); pOnPopulateSceneCallback.onPopulateSceneFinished(); }
它是如何工作的...
与本章中的其他方法一样,我们首先创建常见的BaseGameActivity
变量。对于这个方法,我们还定义了一个Rectangle
对象spinningRect
,它将以每秒特定的圈数旋转。有关创建 AndEngine 活动的更多信息,请参见第一章 AndEngine 游戏结构中的了解生命周期方法。
在第三步中,我们通过首先创建我们的spinningRect
矩形来填充onPopulateScene()
方法,然后我们使用它来注册我们的更新处理程序。在更新处理程序的onUpdate()
方法内部,我们将矩形的旋转设置为等于其当前的旋转,通过getRotation()
方法,加上一个计算,将pSecondsElapsed
变量调整为每秒设定的圈数。下图展示了我们游戏中的更新并不具有相等的持续时间,因此必须利用pSecondsElapsed
参数而不是一个恒定值:
还有更多...
我们在更新处理程序的onUpdate()
方法中使用的计算使Rectangle
对象以每秒半圈的速度旋转。如果我们把计算中的(pSecondsElapsed*360f)
部分乘以4
,矩形将以每秒 4 圈的速度旋转。对于基于时间的线性移动,只需将所需的每秒像素数与pSecondsElapsed
变量相乘。
另请参阅
-
本章节将开始介绍更新处理程序。
-
了解生命周期在第 第一章 AndEngine 游戏结构中。
第八章:最大化的性能
在本章中,我们将介绍一些提高 AndEngine 应用程序性能的最佳实践。包括以下主题:
-
忽略实体更新
-
禁用背景窗口渲染
-
限制同时播放的音轨数量
-
创建精灵池
-
使用精灵组减少渲染时间
-
禁用实体剔除的渲染
引言
游戏优化在 Google Play 上游戏成功中起着关键作用。如果游戏在用户设备上运行不佳,用户很可能会给出负面评价。不幸的是,由于存在许多不同的设备,而且无法在 Google Play 上有效地大规模限制低端设备,因此最好尽可能优化 Android 游戏。忽略评分,可以公平地说,如果游戏在中端设备上的表现不佳,那么在下载和活跃用户方面将无法达到其全部潜力。本章将介绍一些与 AndEngine 性能问题相关的最有帮助的解决方案。这将帮助我们提高中低端设备的性能,无需牺牲质量。
注意
尽管本章中的方法可以大幅提高我们游戏的性能,但重要的是要记住,清晰高效的代码同样重要。游戏开发是一项非常注重性能的任务,与所有语言一样,有许多小事要做或避免。网上有许多资源涵盖了关于 Java 通用实践以及 Android 特定技巧的好坏话题。
忽略实体更新
在优化游戏方面,游戏开发最重要的规则之一是,不要做不需要做的工作!。在本节中,我们将讨论如何使用setIgnoreUpdate()
方法在我们的实体上,以限制更新线程只更新应该更新的内容,而不是不断更新所有实体,不管我们是否使用它们。
如何操作…
以下setIgnoreUpdate(boolean)
方法允许我们控制哪些实体将通过引擎的更新线程进行更新:
Entity entity = new Entity();
// Ignore updates for this entity
entity.setIgnoreUpdate(true);
// Allow this entity to continue updating
entity.setIgnoreUpdate(false);
工作原理…
如前几章所述,每个子对象的onUpdate()
方法都是通过其父对象调用的。引擎首先更新,调用主Scene
对象的更新方法。然后场景继续调用其所有子对象的更新方法。接下来,场景的子对象将分别调用其子对象的更新方法,依此类推。考虑到这一点,通过在主 Scene 对象上调用setIgnoreUpdate()
,我们可以有效地忽略场景上所有实体的更新。
忽略未使用实体的更新,或者除非发生特定事件否则不做出反应的实体,可以节省大量的 CPU 时间。这对于包含大量实体的场景尤为如此。这可能看起来工作量不大,但请记住,对于每个带有实体修改器或更新处理器的实体,这些对象也必须更新。除此之外,每个实体的子实体也会因为父/子层次结构而继续更新。
最佳实践是为所有屏幕外的或不需要持续更新的实体设置setIgnoreUpdate(true)
。对于可能根本不需要任何更新的精灵,比如场景的背景精灵,我们可以无限期地忽略更新,而不会造成任何问题。在实体需要更新,但不是非常频繁的情况下,例如从炮塔发射的子弹,我们可以在子弹从炮塔飞向目标的过程中启用更新,在不再需要时禁用它。
另请参阅
- 第二章中的了解 AndEngine 实体部分,使用实体
禁用后台窗口渲染
在大多数游戏中,开发者通常更倾向于使用全屏模式。虽然从视觉上看我们并没有发现明显的差异,但安卓操作系统并不会识别哪些应用程序是在全屏模式下运行的。这意味着除非在AndroidManifest.xml
中另外指定,否则后台窗口将继续在我们的应用程序下方绘制。在本主题中,我们将介绍如何禁用后台渲染以提高应用程序的帧率,这主要有利于低端设备。
准备工作...
为了停止后台窗口的渲染,我们首先需要为应用程序创建一个主题。我们将在项目的res/values/
文件夹中添加一个名为theme.xml
的新 xml 文件来实现这一点。
用以下代码覆盖默认 xml 文件中的所有内容,并保存文件:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<style name="Theme.NoBackground" parent="android:Theme">
<item name="android:windowBackground">@null</item>
</style>
</resources>
如何操作...
创建并填写完theme.xml
文件后,我们可以在项目的AndroidManifest.xml
文件中,将主题应用于我们的应用程序标签,从而禁用后台窗口渲染。应用程序标签的属性可能看起来类似于这样:
<application
android:theme="@style/Theme.NoBackground"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
请注意,我们也可以将主题应用于特定的活动,而不是在整个应用程序范围内应用,只需在各个活动标签中添加android:theme="@style/Theme.NoBackground"
代码即可。这对于需要同时使用 AndEngine 视图和原生安卓视图的混合游戏来说最为相关,这些视图跨越了多个活动。
工作原理...
禁用背景窗口渲染是一个简单的任务,主要在旧设备上可以提供一些百分比的性能提升。负责背景窗口的主要代码在theme.xml
文件中找到。通过将android:windowBackground
项设置为 null,我们通知设备,我们希望完全移除背景窗口的渲染,而不是绘制它。
限制同时播放的音轨数量
在 AndEngine 中,声音播放通常不会成为游戏性能的问题。然而,在某些情况下,大量声音可能在非常短的时间内播放,这可能会在旧设备上有时甚至在新设备上造成明显的延迟,这取决于同时播放的声音数量。默认情况下,AndEngine 允许同一Sound
对象在任何给定时间同时播放五个音轨。在本主题中,我们将通过操作EngineOptions
来更改同时播放的音轨数量,以更好地满足我们应用程序的需求。
如何操作...
为了增加或减少每个Sound
对象的同时播放音轨数量,我们必须在活动的onCreateEngineOptions()
方法中对EngineOptions
进行简单的调整:
@Override
public EngineOptions onCreateEngineOptions() {
mCamera = new Camera(0, 0, 800, 480);
EngineOptions engineOptions = new EngineOptions(true,
ScreenOrientation.LANDSCAPE_FIXED, new
FillResolutionPolicy(),mCamera);
engineOptions.getAudioOptions().setNeedsSound(true);
engineOptions.getAudioOptions().getSoundOptions().setMaxSimultaneousStreams(2);
return engineOptions;
}
工作原理…
默认情况下,Engine
对象的AudioOptions
设置为每个Sound
对象允许同时播放五个音轨。在大多数情况下,这对于不重度依赖声音播放的应用程序来说,不会造成明显的性能损失。另一方面,倾向于在碰撞或施加力时产生声音的游戏可能会同时播放大量音轨,特别是在任何给定时间场景中有超过 100 个精灵的游戏中。
限制同时播放的音轨数量是一个容易完成的任务。只需在我们的EngineOptions
上调用getAudioOptions().getSoundOptions().setMaxSimultaneousStreams(n)
,其中n
是每个Sound
对象的最大音轨数量,我们就可以减少在游戏过程中不适宜的时候播放的不必要声音。
另请参阅
- 第一章中的引入声音和音乐部分,AndEngine 游戏结构
创建精灵池
GenericPool
类在考虑到移动平台在硬件资源上的限制时,是 AndEngine 游戏设计中极其重要的部分。在 Android 游戏开发中,要实现长时间游戏体验的流畅,关键在于尽可能少地创建对象。这并不意味着我们应该将屏幕上的对象限制在四个或五个,而是应该考虑回收已经创建的对象。这时对象池就派上用场了。
开始操作…
请参考代码包中名为SpritePool
的类。
如何操作…
GenericPool
类使用了一些有用的方法,使得回收对象以供后续使用变得非常简单。我们将在这里介绍主要使用的方法。
构造SpritePool
类:
public SpritePool(ITextureRegion pTextureRegion, VertexBufferObjectManager pVertexBufferObjectManager){
this.mTextureRegion = pTextureRegion;
this.mVertexBufferObjectManager = pVertexBufferObjectManager;
}
-
分配池项目:
@Override protected Sprite onAllocatePoolItem() { return new Sprite(0, 0, this.mTextureRegion, this.mVertexBufferObjectManager); }
-
获取池项目:
public synchronized Sprite obtainPoolItem(final float pX, final float pY) { Sprite sprite = super.obtainPoolItem(); sprite.setPosition(pX, pY); sprite.setVisible(true); sprite.setIgnoreUpdate(false); sprite.setColor(1,1,1); return sprite; }
-
回收池项目:
@Override protected void onHandleRecycleItem(Sprite pItem) { super.onHandleRecycleItem(pItem); pItem.setVisible(false); pItem.setIgnoreUpdate(true); pItem.clearEntityModifiers(); pItem.clearUpdateHandlers(); }
工作原理...
GenericPool
类的理念非常简单。当我们需要对象时,不是创建新对象并在用完后丢弃它们,而是可以告诉池分配有限数量的对象并存储起来以供后续使用。我们现在可以从池中调用obtainPoolItem()
方法,以获取存储分配的对象之一,在我们的关卡中使用,例如作为敌人。一旦这个敌人被玩家摧毁,我们现在可以调用recyclePoolItem(pItem)
将这个敌人对象送回池中。这使我们能够避免垃圾收集的调用,并有可能大大减少新对象所需的内存。
在如何操作...部分中的四种方法,对于使用普通池来说已经足够。显然,我们必须在使用之前创建池。然后,以下三种方法定义了对象分配、获取对象使用以及对象回收时会发生什么,或者在我们用完后将其送回池中存储,直到我们需要新对象。尽管对象池不仅仅用于精灵对象的回收,但我们会更深入地了解每个方法的用途、工作原理以及原因,从构造函数开始。
在第一步中,我们必须传递给池对象构造函数所需的任何对象。在这种情况下,我们需要获取TextureRegion
和VertexBufferObjectManager
以创建 Sprite 对象。这并不是什么新知识,但请记住,GenericPool
类不仅限于创建精灵的池。我们可以为任何类型的对象或数据类型创建池。关键是要使用池的构造函数作为获取传递给池对象分配所需参数的方法。
在第二步中,我们覆盖了onAllocatePoolItem()
方法。当池需要分配新对象时,它将调用此方法。两种情况是:池最初没有对象,或者所有回收的对象都已获取并在使用中。我们在这个方法中需要处理的是返回对象的新实例。
第三步涉及到使用obtain
方法从对象池中获取一个对象,以便在我们的游戏中使用。我们可以看到,在这种情况下,obtainPoolItem()
方法要求我们传入pX
和pY
参数,这些参数将被精灵的setPosition(pX, pY)
方法使用,以重新定位精灵。然后我们将精灵的visibility
设置为true
,允许更新精灵,以及将颜色设置回初始值白色。在任何情况下,此方法应用于将对象的值重置为默认状态,或者定义对象必要的新属性。在代码中,我们可能会像以下代码片段所示从对象池中获取一个新的精灵:
// obtain a sprite and attach it to the scene at position (10, 10)
Sprite sprite = pool.obtainPoolItem(10, 10);
mScene.attachChild(sprite);
在最后的方法中,我们将从GenericPool
类中使用recyclePoolItem(pItem)
方法,其中pItem
是要回收回对象池中的对象。此方法应处理与禁用游戏内使用的对象相关的所有方面。对于精灵来说,为了在精灵存储在池中时提高性能,我们将可见性设置为 false,忽略对精灵的更新,清除任何实体修饰符和更新处理器,这样在我们获取新精灵时它们就不会仍在运行。
注意
即使不使用对象池,也可以考虑在不再需要的Entity
上使用setVisible(false)
和setIgnoreUpdate(true)
。不断附加和分离Entity
对象可能会给垃圾收集器运行提供机会,并可能在游戏过程中引起帧率的明显卡顿。
还有更多…
创建对象池以处理对象回收对于减少性能卡顿非常重要,但是当游戏首次初始化时,池中不会有任何可用的对象。这意味着,根据池需要在整个关卡中分配以满足最大对象数的对象数量,玩家可能会在游戏的前几分钟内注意到帧率的突然中断。为了避免此类问题,最好在关卡加载时预分配池对象,以避免在游戏过程中创建对象。
为了在加载期间分配大量池对象,我们可以对任何扩展GenericPool
的类调用batchAllocatePoolItems(pCount)
,其中pCount
是我们希望分配的项数。请记住,加载比我们需要的更多的物品是资源的浪费,但如果分配的物品不足,也可能会引起帧率卡顿。例如,为了确定我们的游戏中应分配多少敌方对象,我们可以制定一个公式,比如默认敌方数量乘以关卡难度。然而,所有游戏都是不同的,因此所需的对象创建公式也将不同。
另请参阅
- 第二章中关于使用精灵为场景注入生命的部分
使用精灵组减少渲染时间
精灵组是任何需要在任何时刻处理场景上数百个可见精灵的 AndEngine 游戏的一个很好的补充。SpriteGroup
类允许我们将许多精灵渲染调用分组到有限的 OpenGL 调用中,从而消除大量开销。如果一个校车只接一个孩子,把他们送到学校,然后再接下一个孩子,直到所有的孩子都到学校,这个过程完成所需的时间会更长。使用 OpenGL 绘制精灵也是同样的道理。
开始操作…
请参考代码包中名为ApplyingSpriteGroups
的类。这个示例需要一个名为marble.png
的图像,该图像的宽度为 32 像素,高度为 32 像素。
如何操作…
当在我们的游戏中创建一个SpriteGroup
时,我们可以将它们视为专门用于Sprite
对象的Entity
层。以下步骤说明如何创建并将Sprite
对象附加到SpriteGroup
。
-
创建一个精灵组可以使用以下代码实现:
// Create a new sprite group with a maximum sprite capacity of 500 mSpriteGroup = new SpriteGroup(0, 0, mBitmapTextureAtlas, 500, mEngine.getVertexBufferObjectManager()); // Attach the sprite group to the scene mScene.attachChild(mSpriteGroup);
-
将精灵附加到精灵组同样是一个简单的任务:
// Create new sprite Sprite sprite = new Sprite(tempX, tempY, spriteWidth, spriteHeight, mTextureRegion, mEngine.getVertexBufferObjectManager()); // Attach our sprite to the sprite group mSpriteGroup.attachChild(sprite);
工作原理…
在这个示例中,我们设置了一个场景,将大约 375 个精灵应用到我们的场景中,所有这些都是通过使用mSpriteGroup
对象绘制的。一旦创建了精灵组,我们基本上可以将其视为一个普通实体层,根据需要附加精灵。
-
在我们活动的
onCreateResources(
方法中为我们的精灵创建一个BuildableBitmapTextureAtlas
:// Create texture atlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 32, 32, TextureOptions.BILINEAR); // Create texture region mTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, getAssets(), "marble.png"); // Build/load texture atlas mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0)); mBitmapTextureAtlas.load();
创建用于
SpriteGroup
中的纹理可以像处理普通 Sprite 一样处理。 -
构造我们的
mSpriteGroup
对象并将其应用到场景中:// Create a new sprite group with a maximum sprite capacity of 500 mSpriteGroup = new SpriteGroup(0, 0, mBitmapTextureAtlas, 500, mEngine.getVertexBufferObjectManager()); // Attach the sprite group to the scene mScene.attachChild(mSpriteGroup);
SpriteGroup
需要两个我们尚未处理的新参数。SpriteGroup
是Entity
的一个子类型,因此我们知道前两个参数是用于定位SpriteGroup
的 x 和 y 坐标。第三个参数,我们传递了一个BitmapTextureAtlas
。精灵组只能包含与精灵组共享相同纹理图的精灵!第四个参数是SpriteGroup
能够绘制的最大容量。如果容量是 400,那么我们可以将最多 400 个精灵应用到SpriteGroup
。将容量限制为我们希望绘制的最大精灵数非常重要。超出限制将导致应用程序强制关闭。 -
最后一步是将精灵应用到精灵组。
在这个示例中,我们设置了一个循环,以便将精灵应用到屏幕上的各个位置。然而,在这里我们真正关心的是以下用于创建Sprite
并将其附加到SpriteGroup
的代码:
Sprite sprite = new Sprite(tempX, tempY, spriteWidth, spriteHeight, mTextureRegion, mEngine.getVertexBufferObjectManager());
// Attach our sprite to the sprite group
mSpriteGroup.attachChild(sprite);
我们可以像创建任何其他精灵一样创建我们的精灵。我们可以像平常一样设置位置、缩放和纹理区域。现在要做好准备迎接棘手的部分!我们必须调用mSpriteGroup.attachChild(sprite)
,以允许mSpriteGroup
对象处理精灵对象的绘制。这就完成了!
按照这些步骤,我们可以在性能下降之前成功让我们的精灵组在屏幕上绘制许多精灵。与使用单独缓冲区单独绘制精灵相比,差异是巨大的。在许多情况下,用户声称在使用包含大量实体同时出现在场景中的游戏时,可以实现高达 50%的性能提升。
还有更多…
现在还不是将所有项目转换为使用精灵组的时候!使用精灵组的好处不言而喻,但这并不意味着没有负面影响。SpriteGroup
类并不直接得到 OpenGL 的支持。这个类或多或少是一个'hack',它让我们在额外的渲染调用中节省一些时间。在更复杂的项目中设置精灵组可能会因为'副作用'而变得麻烦。
在多次附着和分离利用了 alpha 修饰符和修改了可见性的许多精灵后,有时会出现一些情况,导致精灵组中的某些精灵出现'闪烁'。在越来越多的精灵被附着和分离,或者多次设置为不可见/可见之后,这种结果最为明显。有一种方法可以绕过这个问题,而且不会过多影响性能,即移动精灵使其离开屏幕,而不是从图层中分离它们或设置为不可见。然而,对于只利用一个活动并且根据当前关卡切换场景的大型游戏来说,将精灵移出屏幕可能会带来未来的问题。
在决定使用精灵组之前,要考虑这一点并明智地计划。在将精灵组整合到游戏中之前,测试你打算如何使用精灵的精灵组可能也会有所帮助。精灵组不总会引起问题,但这是需要记住的一点。此外,AndEngine 是一个开源项目,它正在不断更新和改进。关注最新修订版以获取修复或改进。
另请参阅
-
第二章中的了解 AndEngine 实体部分,使用实体
-
第二章中的用精灵为场景注入生命部分,使用实体
使用实体剔除来禁用渲染
剔除实体是一种防止不必要的实体被渲染的方法。在精灵在 AndEngine Camera
的视图中不可见的情况下,这可以提高性能。
如何操作…
对任何预先存在的Entity
或Entity
子类型进行以下方法调用:
entity.setCullingEnabled(true);
它是如何工作的…
剔除实体会根据它们在场景中的位置相对于摄像机可见场景部分来禁止某些实体被渲染。当我们场景上有许多精灵可能会偶尔移出摄像机视野时,这非常有用。启用剔除后,那些在摄像机视图之外的实体将不会被渲染,以避免我们进行不必要的 OpenGL 调用。
请注意,剔除只发生在那些完全在摄像机视野之外的实体上。这考虑了实体的整个区域,从左下角到右上角。如果实体的部分在摄像机视野之外,不会应用剔除。
还有更多内容...
剔除只会停止渲染那些移出摄像机视野的实体。因此,对所有那些经常移出Camera
区域的 游戏对象(如物品、敌人等)启用剔除并不是一个坏主意。对于由较小纹理组成的大型背景实例,剔除也可以显著提高性能,尤其是考虑到背景图像的大小。
剔除确实可以帮助我们节省渲染时间,但这并不意味着我们应该对所有实体启用剔除。毕竟,默认不启用它是有一个原因的。在 HUD 实体上启用剔除是一个糟糕的主意。对于暂停菜单或其他可能进出摄像机视野的大型实体来说,包含它似乎是一个可行的选择,但这可能会导致在移动摄像机时出现问题。AndEngine 的工作方式是 HUD 实际上永远不会随着摄像机移动,所以如果我们对 HUD 实体启用剔除,然后将摄像机向右移动 800 像素(假设我们的摄像机宽度是 800 像素),我们的 HUD 实体仍然会在物理上响应它们在屏幕上的正确位置,但它们不会渲染。它们仍然会响应触摸事件和其他各种场景,但我们就是看不到它们在屏幕上。
此外,在实体被绘制在场景上之前,剔除还需要进行一层额外的可见性检查。因此,较旧的设备在启用实体剔除时,如果这些实体没有被剔除,可能会有性能损失。这可能听起来不多,但当我们在仅能勉强运行 30 帧每秒的设备上有玩家运行时,对例如 200 个精灵进行额外的可见性检查可能会足以使游戏体验变得不便。
参见:
- 第二章中关于理解 AndEngine 实体的部分,使用实体。
第九章:AndEngine 扩展概述
在本章中,我们将介绍一些 AndEngine 最受欢迎的扩展的目的和用法。本章包括以下主题:
-
创建动态壁纸
-
使用多人游戏扩展进行网络通信
-
使用可伸缩矢量图形(SVG)创建高分辨率图形
-
使用 SVG 纹理区域进行颜色映射
简介
在扩展概述章节中,我们将开始使用一些 AndEngine 没有打包的类。有许多扩展可以编写,以添加各种改进或额外功能到任何默认的 AndEngine 游戏。在本章中,我们将使用三个主要扩展,它们将允许我们使用 AndEngine 创建动态壁纸,创建允许多个设备直接相互连接或连接到专用服务器的在线游戏,并最终将 SVG 文件作为纹理区域整合到我们的游戏中,从而在游戏中实现高分辨率和可伸缩的图形。
AndEngine 包含一个相对较长的扩展列表,我们可以将这些扩展包含在项目中,以便使某些任务更容易完成。不幸的是,由于扩展的数量和一些扩展的当前状态,我们限制在本章中包含的扩展数量。然而,大多数 AndEngine 扩展相对容易使用,并且包含可以从 Nicolas Gramlich 的公共 GitHub 仓库获取的示例项目——github.com/nicolasgramlich
。以下是其他 AndEngine 扩展的列表以及简短的用途描述:
-
AndEngineCocosBuilderExtension
:这个扩展允许开发者通过使用所见即所得(WYSIWYG)的概念来创建游戏。这种方法允许开发者在使用 CocosBuilder 软件为桌面电脑的 GUI 拖放环境中构建应用程序。这个扩展可以帮助将菜单和关卡设计简化为在屏幕上放置对象,并将设置导出到一个可以通过AndEngineCocosBuilderExtension
扩展读取的文件。 -
AndEngineAugmentedRealityExtension
:增强现实扩展允许开发者轻松地将一个普通的 AndEngine 活动转换为一个增强现实活动,它将在屏幕上显示设备的物理摄像头视图。然后我们能够将实体附着在屏幕上显示的摄像头视图之上。 -
AndEngineTexturePackerExtension
:这个扩展允许开发者导入通过 TexturePacker 程序为桌面电脑创建的精灵表。这个程序通过让我们将图片拖放到程序中,将完成的精灵表导出为 AndEngine 可读取的格式,然后使用AndEngineTexturePackerExtension
扩展简单地将它加载到我们的项目中,使得创建精灵表变得非常简单。 -
AndEngineTMXTiledMapExtensions
:这个扩展可以在基于图块地图样式的游戏中大大提高生产力。使用 TMX 图块地图编辑器,开发者只需将精灵/图块拖放到基于网格的关卡编辑器中即可创建关卡。一旦在编辑器中创建了一个关卡,只需将其导出为.tmx
文件格式,然后使用AndEngineTMXTiledMapExtension
将关卡加载到我们的项目中。
创建动态壁纸
动态壁纸扩展是 AndEngine 提供的 Android 开发资源中的一个很好的补充。使用这个扩展,我们可以轻松地通过使用我们习惯于游戏开发的所有普通 AndEngine 类来创建壁纸。在本主题中,我们将创建一个包含简单粒子系统的动态壁纸,该粒子系统在屏幕顶部生成粒子。壁纸设置将包括一个允许用户增加粒子移动速度的值。
注意
本教程假设您至少具备 Android SDK 的 Activity
类的基本知识,以及对 Android 视图对象(如 SeekBars
和 TextViews
)的一般了解。
准备就绪
动态壁纸不是典型的 Android 活动。相反,它们是一个服务,在项目设置方面需要略有不同的方法。在访问代码之前,让我们继续创建动态壁纸所需的文件夹和文件。
注意
参考代码捆绑包中名为 LiveWallpaperExtensionExample
的项目。
我们将在下一节介绍每个文件中驻留的代码:
-
在
res/layout
文件夹中创建或覆盖当前的main.xml
文件,将其命名为settings_main.xml
。这个布局文件将用于创建用户调整壁纸属性设置活动的布局。 -
在
res
文件夹中创建一个名为xml
的新文件夹。在这个文件夹内,创建一个新的xml
文件,并将其命名为wallpaper.xml
。这个文件将用作壁纸图标的引用,以及描述和设置活动的引用,该设置活动将用于修改壁纸属性。
如何操作…
我们将从填充所有 XML 文件开始,以便容纳一个动态壁纸服务。这些文件包括 settings_main.xml
、wallpaper.xml
,最后是 AndroidManifest.xml
。
-
创建
settings_main.xml
布局文件:第一步涉及将
settings_main.xml
文件定义为壁纸设置活动的布局。没有限制开发者使用特定布局样式的规则,但对于动态壁纸来说,最常见的方法是使用一个简单的TextView
和相应的Spinner
来提供修改动态壁纸可调整值的方式。 -
打开
res/xml/
文件夹中的wallpaper.xml
文件。将以下代码导入wallpaper.xml
:<?xml version="1.0" encoding="utf-8"?> <wallpaper android:settingsActivity="com.Live.Wallpaper.Extension.Example.LiveWallpaperSettings" android:thumbnail="@drawable/ic_launcher"/>
-
修改
AndroidManifest.xml
以满足壁纸服务的需求:在第三步中,我们必须修改
AndroidManifest.xml
,以便允许我们的项目作为壁纸服务运行。在项目的AndroidManifest.xml
文件中,替换<manifest>
标签内的所有代码,使用以下内容:<uses-feature android:name="android.software.live_wallpaper" /> <application android:icon="@drawable/ic_launcher" > <service android:name=".LiveWallpaperExtensionService" android:enabled="true" android:icon="@drawable/ic_launcher" android:label="@string/service_name" android:permission="android.permission.BIND_WALLPAPER" > <intent-filter android:priority="1" > <action android:name="android.service.wallpaper.WallpaperService" /> </intent-filter> <meta-data android:name="android.service.wallpaper" android:resource="@xml/wallpaper" /> </service> <activity android:name=".LiveWallpaperSettings" android:exported="true" android:icon="@drawable/ic_launcher" android:label="@string/live_wallpaper_settings" android:theme="@android:style/Theme.Black" > </activity>
处理完这三个 xml 文件后,我们可以创建实时壁纸所需的类。我们将使用三个类来处理实时壁纸的执行。这些类是LiveWallpaperExtensionService.java
、LiveWallpaperSettings.java
和LiveWallpaperPreferences.java
,在以下步骤中将会介绍:
-
创建实时壁纸偏好设置类:
LiveWallpaperPreferences.java
类与我们在第一章,AndEngine 游戏结构中讨论的偏好设置类相似。在这种情况下,偏好设置类的主要目的是处理生成的粒子的速度值。以下方法用于保存和加载粒子的速度值。请注意,我们取反了mParticleSpeed
值,因为我们希望粒子向屏幕底部移动:// Return the saved value for the mParticleSpeed variable public int getParticleSpeed(){ return -mParticleSpeed; } // Save the mParticleSpeed value to the wallpaper's preference file public void setParticleSpeed(int pParticleSpeed){ this.mParticleSpeed = pParticleSpeed; this.mSharedPreferencesEditor.putInt(PARTICLE_SPEED_KEY, mParticleSpeed); this.mSharedPreferencesEditor.commit(); }
-
创建实时壁纸设置活动:
实时壁纸的设置活动扩展了 Android SDK 的
Activity
类,使用settings_main.xml
文件作为活动的布局。此活动的目的是根据SeekBar
对象的进度为mParticleSpeed
变量获取一个值。一旦退出设置活动,mParticleSpeed
值就会被保存到我们的偏好设置中。 -
创建实时壁纸服务:
为设置实时壁纸而涉及的最终步骤是创建
LiveWallpaperExtensionService.java
类,其中包含实时壁纸服务的代码。为了指定我们希望该类使用实时壁纸扩展类,我们只需在LiveWallpaperExtensionService.java
声明中添加extends BaseLiveWallpaperService
。完成这一步后,我们可以看到,设置BaseLiveWallpaperService
类与从这时起设置BaseGameActivity
类非常相似,这使我们能够加载资源、应用精灵,或我们已经习惯的任何其他常见的 AndEngine 任务。
工作原理…
如果我们从整个项目来看,这个“配方”相当大,但幸运的是,与类文件相关的代码在之前的章节中已经讨论过了,所以不必担心!为了简洁起见,我们将省略在之前章节中已经讨论过的类。如果需要复习,请查看查看更多...小节中提到的主题。
在第一步中,我们要做的就是创建一个最小的 Android xml
布局,用于设置活动。完全有可能跳过这一步,使用 AndEngine 的BaseGameActivity
作为设置活动,但为了简化问题,我们采用了非常基本的TextView/SeekBar
方法。这对开发人员来说节省了时间,对用户来说也更加方便。尽量保持这个屏幕简洁,因为它应该是一个简单屏幕,有简单的目的。
在第二步中,我们将创建一个wallpaper.xml
文件,该文件将作为AndroidManifest.xml
文件中动态壁纸服务所需的一些规范的引用。这个文件仅仅用于存储服务的属性,这些属性包括包和类名,或者按下壁纸预览中的设置...按钮时要启动的设置活动的“链接”。wallpaper.xml
还包括对壁纸选择窗口中要使用的图标的引用。
在第三步中,我们正在修改AndroidManifest.xml
文件,以便将动态壁纸服务作为本项目的主组件运行,而不是启动一个活动。在<service>
标签内,我们为壁纸服务包含了name
、icon
和label
属性。这些属性与活动中的属性具有相同的目的。另外两个属性是android:enabled="true"
,这意味着我们希望默认启用壁纸服务,以及android:permission="android.permission.BIND_WALLPAPER"
属性,这意味着只有 Android 操作系统可以绑定到该服务。活动的属性与此类似,只是我们包括了exported
和theme
属性,并排除了enabled
和permission
属性。android:exported="true"
属性表示活动可以通过外部进程启动,而主题属性将改变设置活动 UI 的外观。
第四步涉及创建我们将用于存储用户可调整值的偏好设置类。在这个食谱中,我们在偏好设置类中包含了一个名为mParticleSpeed
的单个值,并带有相应的获取器和设置器方法。在一个更复杂的动态壁纸中,我们可以在此基础上构建这个类,使我们能够轻松添加或移除变量,为壁纸提供尽可能多的自定义属性。
在第五步中,我们创建了一个Activity
类,当用户在动态壁纸预览屏幕上按下设置...按钮时显示。在这个特定的Activity
中,我们获取了settings_main.xml
文件作为我们的布局,其中包含两个用于显示标签和相应值的TextView
视图类型,以及一个允许操作壁纸可调整值的SeekBar
。这个Activity
最重要的任务是当用户选择理想的速度后,能够将设置保存到偏好文件中。这是通过在SeekBar
意识到用户移动了SeekBar
滑块时调整mParticleSpeed
变量来完成的:
// OnProgressChanged represents a movement on the slider
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// Set the mParticleSpeed depending on the SeekBar's position(progress)
mParticleSpeed = progress;
在此事件中,除了更新mParticleSpeed
值,相关的TextView
也会被更新。然而,这个值实际上只有在用户离开设置活动时才会保存到偏好文件中,以避免不必要地覆盖偏好文件。为了将新值保存到偏好文件,我们可以在Activity
类最小化时从LiveWallpaperPreferences
单例调用setParticleSpeed(mParticleSpeed)
:
@Override
protected void onPause() {
// onPause(), we save the current value of mParticleSpeed to the preference file.
// Anytime the wallpaper's lifecycle is executed, the mParticleSpeed value is loaded
LiveWallpaperPreferences.getInstance().setParticleSpeed(mParticleSpeed);
super.onPause();
}
在第六步也是最后一步中,我们终于可以开始编写动态壁纸的视觉部分。在这款特定的壁纸中,我们在视觉吸引力方面保持了简单,但我们确实涵盖了开发壁纸所需的所有必要信息。如果我们查看LiveWallpaperExtensionService
类,需要关注的一些关键变量包括以下内容:
private int mParticleSpeed;
// These ratio variables will be used to keep proper scaling of entities
// regardless of the screen orientation
private float mRatioX;
private float mRatioY;
尽管在其他类解释中我们已经讨论了mParticleSpeed
变量,但此时应该很清楚,我们将使用这个变量来最终确定粒子的速度,因为这是将处理ParticleSystem
对象的类。上面声明的另外两个'比例'变量是为了帮助我们保持实体的适当缩放比例。这些变量在用户将设备从横屏倾斜到竖屏或反之亦然时是必需的,这样我们就可以根据表面视图的宽度和高度计算粒子的比例,以防止实体在方向改变时被拉伸或扭曲。跳到这个类的底部覆盖方法,以下代码确定了mRatioX
和mRatioY
的值:
@Override
public void onSurfaceChanged(GLState pGLState, int pWidth, int pHeight) {
if(pWidth > pHeight){
mRatioX = 1;
mRatioY = 1;
} else {
mRatioX = ((float)pHeight) / pWidth;
mRatioY = ((float)pWidth) / pHeight;
}
super.onSurfaceChanged(pGLState, pWidth, pHeight);
}
我们可以在这里看到,if
语句正在检查设备是否处于横屏或竖屏模式。如果pWidth
大于pHeight
,这意味着当前的方向是横屏模式,将 x 和 y 的比例尺设置为默认值 1。另一方面,如果设备设置为竖屏模式,那么我们必须重新计算粒子实体的比例尺。
当处理完onSurfaceChanged()
方法后,我们继续讨论剩余的关键点,下一个是偏好设置管理。处理偏好设置是一项相当琐碎的任务。首先,我们应该初始化偏好设置文件,以防这是第一次启动壁纸。我们通过在onCreateEngineOptions()
中的LiveWallpaperPreferences
实例调用initPreferences(this)
方法来实现这一点。我们还需要重写onResume()
方法,以便通过从LiveWallpaperPreferences
实例调用getParticleSpeed()
方法,用偏好设置文件中存储的值加载mParticleSpeed
变量。
最后,我们来到实时壁纸设置的最后一个步骤,即设置粒子系统。这个特定的粒子系统并不特别花哨,但它包括一个ParticleModifier
对象,其中有一些需要注意的点。由于我们将IParticleModifier
接口添加到粒子系统中,因此我们可以在每次更新每个粒子时访问由系统生成的单个粒子。在onUpdateParticle()
方法中,我们将根据从偏好设置文件中加载的mParticleSpeed
变量设置粒子的速度:
// speed set by the preferences...
if(currentVelocityY != mParticleSpeed){
// Adjust the particle's velocity to the proper value
particlePhysicsHandler.setVelocityY(mParticleSpeed);
}
如果粒子的比例不等于mRatioX/mRatioY
值,我们还必须调整粒子的比例,以补偿设备方向:
// If the particle's scale is not equal to the current ratio
if(entity.getScaleX() != mRatioX){
// Re-scale the particle to better suit the current screen ratio
entity.setScale(mRatioX, mRatioY);
}
这样就完成了使用 AndEngine 设置实时壁纸的全部工作!尝试玩转粒子系统,在设置中添加新的可自定义值,看看你能想出什么。使用这个扩展,你将能够快速上手,立即创建新的实时壁纸!
另请参阅…
-
第一章中的保存和加载游戏数据部分,AndEngine 游戏结构。
-
第二章中的使用粒子系统部分,使用实体。
使用多人游戏扩展进行网络编程
这里无疑是最受欢迎的游戏设计方面。这当然是多人游戏。在这个项目配方中,我们将使用 AndEngine 的多玩家扩展,以便直接在移动设备上创建一个完全功能性的客户端和服务器。一旦我们介绍了这个扩展包括的类和特性,以简化网络编程,你将能够将你的在线游戏想法变为现实!
准备就绪
创建一个多人游戏可能需要相当多的组件,以满足项目的可读性。
注意
请参考代码包中的名为MultiplayerExtensionExample
的项目。
因此,我们将把这些不同的组件分为五个类别。
创建一个名为MultiplayerExtensionExample
的新 Android 项目。项目准备就绪后,创建四个具有以下名称的新类文件:
-
MultiplayerExtensionExample.java
:本食谱的BaseGameActivity
类 -
MultiplayerServer.java
:包含主要服务器组件的类 -
MultiplayerClient.java
:包含主要客户端组件的类 -
ServerMessages.java
:包含旨在从服务器发送到客户端的消息的类 -
ClientMessages.java
:包含旨在从客户端发送到服务器的消息的类
打开项目的AndroidManifest.xml
文件,并添加以下两个<uses-permission>
属性:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
如何操作...
为了保持本食谱中内容的相对性,我们将按照准备就绪部分提到的顺序,依次处理每个类,从MultiplayerExtensionExample
类开始。
-
为
mMessagePool
声明并注册服务器/客户端消息:this.mMessagePool.registerMessage(ServerMessages.SERVER_MESSAGE_ADD_POINT, AddPointServerMessage.class); this.mMessagePool.registerMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT, AddPointClientMessage.class);
-
配置场景触摸监听器,以允许与服务器之间的消息发送和接收:
if (pSceneTouchEvent.getAction() == TouchEvent.ACTION_MOVE) { if (mServer != null) { if(mClient != null){ // Obtain a ServerMessage object from the mMessagePool AddPointServerMessage message = (AddPointServerMessage) MultiplayerExtensionExample.this.mMessagePool.obtainMessage(ServerMessages.SERVER_MESSAGE_ADD_POINT); // Set up the message with the device's ID, touch coordinates and draw color message.set(SERVER_ID, pSceneTouchEvent.getX(), pSceneTouchEvent.getY(), mClient.getDrawColor()); // Send the client/server's draw message to all clients mServer.sendMessage(message); // Recycle the message back into the message pool MultiplayerExtensionExample.this.mMessagePool.recycleMessage(message); return true; } // If device is running as a client... } else if(mClient != null){ /* Similar to the message sending code above, except * in this case, the client is *not* running as a server. * This means we have to first send the message to the server * via a ClientMessage rather than ServerMessage */ AddPointClientMessage message = (AddPointClientMessage) MultiplayerExtensionExample.this.mMessagePool.obtainMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT); message.set(CLIENT_ID, pSceneTouchEvent.getX(), pSceneTouchEvent.getY(), mClient.getDrawColor()); mClient.sendMessage(message); MultiplayerExtensionExample.this.mMessagePool.recycleMessage(message); return true; } }
-
创建一个开关对话框,提示用户选择作为服务器或客户端。如果选择了服务器或客户端组件,我们将初始化这两个组件中的一个:
mServer = new MultiplayerServer(SERVER_PORT); mServer.initServer(); // or... mClient = new MultiplayerClient(mServerIP,SERVER_PORT, mEngine, mScene); mClient.initClient();
-
重写活动的
onDestroy()
方法,在活动被销毁时终止服务器和客户端组件:@Override protected void onDestroy() { // Terminate the client and server socket connections // when the application is destroyed if (this.mClient != null) this.mClient.terminate(); if (this.mServer != null) this.mServer.terminate(); super.onDestroy(); }
一旦所有主要活动的功能就位,我们可以继续编写服务器端代码。
-
创建服务器的初始化方法——创建处理服务器客户端连接的
SocketServer
对象:// Create the SocketServer, specifying a port, client listener and // a server state listener (listeners are implemented in this class) MultiplayerServer.this.mSocketServer = new SocketServer<SocketConnectionClientConnector>( MultiplayerServer.this.mServerPort, MultiplayerServer.this, MultiplayerServer.this) { // Handle client connection here... };
-
处理客户端连接到服务器。这涉及到注册客户端消息并定义如何处理它们:
// Called when a new client connects to the server... @Override protected SocketConnectionClientConnector newClientConnector( SocketConnection pSocketConnection) throws IOException { // Create a new client connector from the socket connection final SocketConnectionClientConnector clientConnector = new SocketConnectionClientConnector(pSocketConnection); // Register the client message to the new client clientConnector.registerClientMessage(ClientMessages.CLIENT_MESSAGE_ADD_POINT, AddPointClientMessage.class, new IClientMessageHandler<SocketConnection>(){ // Handle message received by the server... @Override public void onHandleMessage( ClientConnector<SocketConnection> pClientConnector, IClientMessage pClientMessage) throws IOException { // Obtain the client message AddPointClientMessage incomingMessage = (AddPointClientMessage) pClientMessage; // Create a new server message containing the contents of the message received // from a client AddPointServerMessage outgoingMessage = new AddPointServerMessage(incomingMessage.getID(), incomingMessage.getX(), incomingMessage.getY(), incomingMessage.getColorId()); // Reroute message received from client to all other clients sendMessage(outgoingMessage); } }); // Return the new client connector return clientConnector; }
-
声明并初始化了
SocketServer
对象后,我们需要调用其start()
方法:// Start the server once it's initialized MultiplayerServer.this.mSocketServer.start();
-
创建
sendMessage()
服务器广播方法:// Send broadcast server message to all clients public void sendMessage(ServerMessage pServerMessage){ try { this.mSocketServer.sendBroadcastServerMessage(pServerMessage); } catch (IOException e) { e.printStackTrace(); } }
-
创建
terminate()
方法以关闭连接:// Terminate the server socket and stop the server thread public void terminate(){ if(this.mSocketServer != null) this.mSocketServer.terminate(); }
服务器端代码完成后,我们将在
MultiplayerClient
类中继续实现客户端代码。这个类与MultiplayerServer
类非常相似,因此我们将省略不必要的客户端步骤。 -
创建
Socket
、SocketConnection
,最后创建ServerConnector
以与服务器建立连接:// Create the socket with the specified Server IP and port Socket socket = new Socket(MultiplayerClient.this.mServerIP, MultiplayerClient.this.mServerPort); // Create the socket connection, establishing the input/output stream SocketConnection socketConnection = new SocketConnection(socket); // Create the server connector with the specified socket connection // and client connection listener MultiplayerClient.this.mServerConnector = new SocketConnectionServerConnector(socketConnection, MultiplayerClient.this);
-
处理从服务器接收到的消息:
// obtain the class casted server message AddPointServerMessage message = (AddPointServerMessage) pServerMessage; // Create a new Rectangle (point), based on values obtained via the server // message received Rectangle point = new Rectangle(message.getX(), message.getY(), 3, 3, mEngine.getVertexBufferObjectManager()); // Obtain the color id from the message final int colorId = message.getColorId();
-
创建客户端和服务器消息:
ClientMessage
和ServerMessage
旨在作为数据包,能够被发送到服务器和客户端,以及从服务器和客户端接收。在这个食谱中,我们将为客户端和服务器创建一个消息,以处理发送关于在客户端设备上绘制点的信息。这些消息中存储的变量包括:// Member variables to be read in from the server and sent to clients private int mID; private float mX; private float mY; private int mColorId;
读取和写入通信数据就像以下这样简单:
// Apply the read data to the message's member variables @Override protected void onReadTransmissionData(DataInputStream pDataInputStream) throws IOException { this.mID = pDataInputStream.readInt(); this.mX = pDataInputStream.readFloat(); this.mY = pDataInputStream. readFloat(); this.mColorId = pDataInputStream.readInt(); } // Write the message's member variables to the output stream @Override protected void onWriteTransmissionData( DataOutputStream pDataOutputStream) throws IOException { pDataOutputStream.writeInt(this.mID); pDataOutputStream.writeFloat(this.mX); pDataOutputStream.writeFloat(this.mY); pDataOutputStream.writeInt(mColorId); }
工作原理...
在本食谱实现的服务器/客户端通信中,我们构建了一个允许直接在移动设备上部署服务器的应用程序。从这里,其他移动设备可以作为客户端连接到前述的移动服务器。一旦服务器与至少一个客户端建立连接,如果任何客户端创建了触摸事件,服务器将开始向所有客户端中继消息,在所有连接的客户端屏幕上绘制点。如果这听起来有些令人困惑,不用害怕,很快一切就会变得清晰。
在前五个步骤中,我们将编写BaseGameActivity
类。这个类是服务器和客户端的入口点,同时也提供了触摸事件功能,使客户端能够在屏幕上绘图。
在第一步中,我们需要将必要的ServerMessage
和ClientMessage
对象注册到我们的mMessagePool
中。mMessagePool
对象是 AndEngine 中MultiPool
类的扩展。关于如何使用MessagePool
类回收通过网络发送和接收的消息,请参阅第八章《最大化性能》中的创建精灵池部分。
在第二步中,我们通过设置一个场景触摸监听器接口来建立场景,该接口的目的是发送跨网络的消息。在触摸监听器内部,我们可以使用简单的条件语句来检查设备是否作为客户端或服务器运行,通过if(mServer != null)
这行代码,如果设备作为服务器运行则返回 true。此外,我们可以调用if(mClient != null)
来检查设备是否作为客户端运行。在服务器检查中嵌套的客户端检查,如果设备同时作为客户端和服务器运行,将返回 true。如果设备作为客户端运行,发送消息只需从mMessagePool
获取一条新消息,在消息上调用set(device_id, touchX, touchY, colorId)
方法,然后调用mClient.sendMessage(message)
。消息发送后,我们应该始终将其回收至池中,以免浪费内存。在继续之前,最后要提到的一点是,在嵌套的客户端条件中,我们发送的是服务器消息而不是客户端消息。这是因为在这种情况下,客户端同时也是服务器。这意味着我们可以跳过向服务器发送客户端消息,因为服务器已经包含了触摸事件数据。
第三步对于大多数开发者来说可能并不是理想的情况,因为我们使用对话框作为选择设备是作为服务器还是客户端的手段。这个场景仅用于展示如何初始化组件,所以对话框并不一定重要。选择用户是否能够主持游戏取决于游戏类型和开发者的想法,但这个方案至少涵盖了如何设置服务器(如果需要的话)。请记住,在初始化服务器时,我们只需要知道端口号。另一方面,客户端需要知道有效的服务器 IP和服务器端口才能建立连接。一旦使用这些参数构建了MultiplayerServer
和/或MultiplayerClient
类,我们就可以初始化组件。初始化的目的将在不久后介绍。
对于BaseGameActivity
类的第四步也是最后一步,是允许活动在调用onDestroy()
时终止MultiplayerServer
和MultiplayerClient
的连接。这将关闭通信线程和套接字,在应用程序被销毁之前。
接下来,我们看看第五步中的MultiplayerServer
代码,了解服务器的初始化。在创建服务器用来监听新客户端连接的SocketServer
对象时,我们必须传入服务器的端口号,以及一个ClientConnectorListener
和一个SocketServerListener
。MultiplayerServer
类实现了这两个监听器,记录服务器启动、停止、客户端连接到服务器以及客户端断开连接时的日志。
在第六步中,我们正在实施处理服务器如何响应传入连接以及如何处理客户端接收到的消息的系统。以下是按应实施顺序涉及的过程:
-
当新客户端连接到服务器时,将调用
protected SocketConnectionClientConnector newClientConnector(...)
。 -
创建一个新的
SocketConnectionClientConnector
供客户端用作新客户端与服务器之间的通信手段。 -
通过
registerClientMessage(flag, message.class, messageHandlerInterface)
注册你希望服务器识别的ClientMessages
。 -
在
messageHandlerInterface
接口的onHandleMessage()
方法中,我们处理从网络接收到的任何消息。在这种情况下,服务器只是将客户端的消息中继回所有连接的客户端。 -
返回新的
clientConnector
对象。
这些点概述了服务器/客户端通信的主要功能。在这个示例中,我们使用单一消息在客户端设备上绘制点,但对于更广泛的消息范围,只要标志参数与我们在onHandleMessage()
接口中获得的消息类型匹配,我们就可以继续调用registerClientMessage()
方法。一旦注册了所有适当的消息,并且我们完成了客户端处理代码,我们可以继续第七步,在mSocketServer
对象上调用start()
。
在第八步中,我们为服务器创建了sendMessage(message)
方法。服务器的sendMessage(message)
版本通过简单地遍历客户端连接器列表,向每个连接器调用sendServerMessage(message)
,向所有客户端发送广播消息。如果我们希望向单个客户端发送服务器消息,可以直接在单个ClientConnector
上调用sendServerMessage(message)
。在另一端,我们有客户端版本的sendMessage(message)
。客户端的sendMessage()
方法实际上并不向其他客户端发送消息;实际上,客户端根本不与其他客户端通信。客户端的工作是与服务器通信,然后服务器再与其他客户端通信。查看以下图表以更好地了解我们的网络通信是如何工作的:
在前述图中,流程由数字标出。首先,客户端将消息发送到服务器。一旦服务器接收到消息,它将遍历其客户端列表中的每个ClientConnector
对象,向所有客户端发送广播。
创建MultiplayerServer
组件的最后一步是创建一个用于终止mSocketServer
的方法。此方法由我们主活动中的onDestroy()
调用,以便在我们使用完毕后销毁通信线程。
服务器端的所有代码准备就绪后,我们可以继续编写客户端代码。MultiplayerClient
的代码与服务器端有些相似,但存在一些差异。在与服务器建立连接时,我们必须比服务器初始化时更具体一些。首先,我们必须创建一个新的 Socket,指定要连接的 IP 地址以及服务器端口号。然后,我们将Socket
传递给一个新的SocketConnection
对象,用于在 socket 上建立输入/输出流。完成此操作后,我们可以创建我们的ServerConnector
,其目的是在客户端和服务器之间建立最终的连接。
现在我们已经接近一个完整的客户端/服务器通信项目了!第 11 步是真正的魔法发生的地方——客户端接收服务器消息。为了接收服务器消息,类似于服务器接收消息的实现,我们只需调用mServerConnector.registerServerMessage(...)
,这会给我们一个填充onHandleMessage(serverConnector, serverMessage)
接口的机会。同样,类似于服务器端的实现,我们可以将serverMessage
对象强制转换为AddPointServerMessage
类,这样我们就能获取到消息中存储的自定义值。
现在,我们已经将所有服务器和客户端代码处理完毕,来到了最后一步。这当然就是创建将用于MessagePool
的消息,以及我们一直在到处发送和接收的对象。我们需要了解两种不同类型的消息对象。第一种是ServerMessage
,它包括那些从客户端发送并由服务器接收/读取的消息。另一种消息,你已经猜到了,是ClientMessage
,它用于从服务器发送并由客户端接收/读取。通过创建我们自己的消息类,我们可以轻松地将由基本数据类型表示的数据块打包并发送到网络中。基本数据类型包括int
、float
、long
、boolean
等。
在这个食谱中使用的消息里,我们存储了一个 ID,用以标识消息是来自客户端还是服务器,每个客户端触摸事件的 x 和 y 坐标,以及当前选定的绘图颜色 ID。每个值都应该有其对应的获取方法,这样我们在接收到消息时就能获取到消息的详细信息。此外,通过覆盖客户端或服务器消息,我们必须实现onReadTransmissionData(DataInputStream)
方法,它允许我们从输入流中获取数据类型并将它们复制到我们的成员变量中。我们还必须实现onWriteTransmissionData(DataOutputStream)
方法,用于将成员变量写入数据流并发送到网络中。在创建服务器和客户端消息时,我们需要注意的一个问题是,接收到的成员变量中的数据是以它们发送时的顺序获取的。请看我们服务器消息的读写方法的顺序:
// write method
pDataOutputStream.writeInt(this.mID);
pDataOutputStream.writeFloat(this.mX);
pDataOutputStream.writeFloat(this.mY);
pDataOutputStream.writeInt(this.mColorId);
// read method
this.mID = pDataInputStream.readInt();
this.mX = pDataInputStream.readFloat();
this.mY = pDataInputStream. readFloat();
this.mColorId = pDataInputStream.readInt();
在记住前面的代码的前提下,我们可以确信,如果我们向输出流中写入包含int
、float
、int
、boolean
和一个float
的消息,任何接收该消息的设备将分别读取一个int
、float
、int
、boolean
和一个float
。
使用 SVG 创建高分辨率图形
将可缩放矢量图形(SVG)集成到我们的移动游戏中,对于开发来说是一个巨大的优势,尤其是在与 Android 平台合作时。最大的好处,也是我们将在本主题中讨论的内容,是 SVG 可以根据运行我们应用的设备进行缩放。不再需要为更大的显示屏创建多个 PNG 图片集,更重要的是,不再需要在大型屏幕设备上处理严重的像素化图形!在本主题中,我们将使用AndEngineSVGTextureRegionExtension
扩展来为我们的精灵创建高分辨率纹理区域。请看下面的截图,左侧是标准分辨率图像的缩放,右侧是 SVG 的效果:
尽管 SVG 资源在创建多种屏幕尺寸的高分辨率图形时可能非常有说服力,但在SVG
扩展当前的状态下,也存在一些缺点。SVG
扩展不会渲染所有可用的元素,例如文本和 3D 形状。然而,大多数必要的元素都是可用的,并且在运行时可以正确加载,如路径、渐变、填充颜色和一些形状。在 SVG 加载过程中未能加载的元素将通过 Logcat 显示。
从 SVG 文件中移除不受SVG
扩展支持的元素是一个明智的选择,因为它们可能会影响加载时间,这是使用SVG
扩展的另一个负面因素。由于 SVG 纹理在加载到内存之前必须先转换为 PNG 格式,因此它们的加载时间将比 PNG 文件长得多。根据每个 SVG 中包含的元素数量,SVG 纹理的加载时间可能会达到等效 PNG 图像的两到三倍。最常见的解决方法是,在应用程序首次启动时将 SVG 纹理以 PNG 格式保存到设备上。随后的每次启动都会加载 PNG 图像,以减少加载时间,同时保持设备特定的图像分辨率。
准备工作
请参考代码包中名为WorkingWithSVG
的项目。
如何操作...
使用 SVG 纹理区域是一个简单易行且效果显著的任务。
-
与普通的
TextureRegion
类似,首先我们需要一个BuildableBitmapTextureAtlas
:// Create a new buildable bitmap texture atlas to build and contain texture regions BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 1024, 1024, TextureOptions.BILINEAR);
-
现在我们已经设置好了纹理图集,可以通过使用
SVGBitmapTextureAtlasTextureRegionFactory
单例来创建 SVG 纹理区域:// Create a low-res (32x32) texture region of svg_image.svg mLowResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 32,32); // Create a med-res (128x128) texture region of svg_image.svg mMedResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 128, 128); // Create a high-res (256x256) texture region of svg_image.svg mHiResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 256,256);
工作原理...
如我们所见,创建一个SVG
纹理区域与普通的TextureRegion
并没有太大区别。两者在实例化方面的唯一真正区别在于,我们必须输入一个width
和height
值作为最后两个参数。这是因为,与平均的栅格图像格式不同,由于固定的像素位置,其宽度和高度或多或少是硬编码的,SVG
像素位置可以按我们喜欢的任何大小进行放大或缩小。如果我们缩放SVG
纹理区域,向量的坐标将简单地调整自己以继续生成清晰、精确的图像。一旦构建了SVG
纹理区域,我们就可以像应用其他任何纹理区域一样将其应用于精灵。
了解如何创建SVG
纹理区域是很好的,但它的意义远不止于此。毕竟,在游戏中使用 SVG 图像的美妙之处在于能够根据设备显示大小来缩放图像。这样,我们就不需要为小屏幕设备加载大图像以适应平板电脑,也不需要通过创建小的纹理区域来节省内存,让平板用户受苦。SVG
扩展实际上使我们能够非常简单地处理根据显示大小进行缩放的概念。以下代码展示了我们如何为所有创建的SVG
纹理区域实现大规模缩放因子。这将使我们避免手动根据显示大小创建不同大小的纹理区域:
float mScaleFactor = 1;
// Obtain the device display metrics (dpi)
DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
int deviceDpi = displayMetrics.densityDpi;
switch(deviceDpi){
case DisplayMetrics.DENSITY_LOW:
// Scale factor already set to 1
break;
case DisplayMetrics.DENSITY_MEDIUM:
// Increase scale to a suitable value for mid-size displays
mScaleFactor = 1.5f;
break;
case DisplayMetrics.DENSITY_HIGH:
// Increase scale to a suitable value for larger displays
mScaleFactor = 2;
break;
case DisplayMetrics.DENSITY_XHIGH:
// Increase scale to suitable value for largest displays
mScaleFactor = 2.5f;
break;
default:
// Scale factor already set to 1
break;
}
SVGBitmapTextureAtlasTextureRegionFactory.setScaleFactor(mScaleFactor);
上述代码可以复制并粘贴到活动的onCreateEngineOptions()
方法中。需要做的就是决定您希望根据设备大小为 SVG 应用哪些缩放因子!从这一点开始,我们可以创建一个单一的SVG
纹理区域,根据显示大小,纹理区域将相应地缩放。例如,我们可以加载如下纹理区域:
mLowResTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "svg_image.svg", 32,32);
我们可以将纹理区域的宽度和高度值定义为32
,但是通过在工厂类中调整缩放因子,对于DENSITY_XHIGH
显示,纹理区域会通过将指定值与缩放因子相乘来构建成80x80
。处理具有自动缩放因子的纹理区域时要小心。缩放还会增加它们在BuildableBitmapTextureAtlas
对象中占用的空间,如果超出限制,可能会像其他任何TextureRegion
一样导致错误。
参见……
- 在第一章,AndEngine 游戏结构中的不同类型的纹理部分。
使用 SVG 纹理区域进行色彩映射
SVG
纹理区域的一个有用特点是,我们可以轻松地映射纹理的颜色。这种技术在允许用户为其角色的角色选择自定义颜色的游戏中很常见,无论是服装和配饰颜色、发色、肤色、地形主题等等。在本主题中,我们将在构建 SVG
纹理区域时使用 ISVGColorMapper
接口,为我们的精灵创建自定义颜色集。
准备工作
在我们开始颜色映射的编码工作之前,需要创建一个带有预设颜色的 SVG 图像。我们可以将这些预设颜色视为我们的映射图。许多开发者中最受欢迎的 SVG 编辑器之一是Inkscape,它是一款免费、易于使用且功能齐全的编辑器。可以从以下链接下载 Inkscape,inkscape.org/download/
,或者你也可以选择使用其他你喜欢的 SVG 编辑器。
如何操作...
颜色映射听起来可能是一项繁琐的工作,但实际上非常容易完成。我们需要做的是保持 SVG
图像与代码之间的一点点一致性。牢记这一点,创建多颜色的单一源纹理可以是一个非常快速的任务。以下步骤包括从绘制 SVG
图像以方便颜色映射,到编写将颜色映射到应用程序中 SVG
图像特定区域的代码的过程。
-
绘制我们的
SVG
图像:为了在运行时轻松地将颜色映射到
SVG
纹理区域,我们需要在选择的编辑器中绘制一个SVG
图像。这涉及到为我们的ISVGColorMapper
接口容易识别而将图像的不同部分进行颜色编码。下图显示了一个带有定义颜色值的形状,这些颜色值显示在图的左侧。 -
实现
ISVGColorMapper
接口:在通过
SVGBitmapTextureAtlasTextureRegionFactory
创建SVG
纹理区域之前,我们将根据我们的SVG
图像定义ISVGColorMapper
接口。如果我们查看以下代码中的条件语句,我们可以看到我们正在检查前一个图中找到的相同颜色值:ISVGColorMapper svgColorMapper = new ISVGColorMapper(){ @Override public Integer mapColor(final Integer pColor) { // If the path contains no color channels, return null if(pColor == null) { return null; } // Obtain color values from 0-255 int alpha = Color.alpha(pColor); int red = Color.red(pColor); int green = Color.green(pColor); int blue = Color.blue(pColor); // If the SVG image's color values equal red, or ARGB{0,255,0,0} if(red == 255 && green == 0 && blue == 0){ // Return a pure blue color return Color.argb(0, 0, 0, 255); // If the SVG image's color values equal green, or ARGB{0,0,255,0} } else if(red == 0 && green == 255 && blue == 0){ // Return a pure white return Color.argb(0, 255, 255, 255); // If the SVG image's color values equal blue, or ARGB{0,0,0,255} } else if(red == 0 && green == 0 && blue == 255){ // Return a pure blue color return Color.argb(0, 0, 0, blue); // If the SVG image's color values are white, or ARGB{0,254,254,254} } else if(red == 254 && blue == 254 && green == 254){ // Return a pure red color return Color.argb(0, 255, 0, 0); // If our "custom color" conditionals do not apply... } else { // Return the SVG image's default color values return Color.argb(alpha, red, green, blue); } } }; // Create an SVG texture region mSVGTextureRegion = SVGBitmapTextureAtlasTextureRegionFactory.createFromAsset(bitmapTextureAtlas, this, "color_mapping.svg", 256,256, svgColorMapper);
-
最后,一旦定义了接口,我们可以在创建纹理区域时将其作为最后一个参数传入。完成这一步后,使用
SVG
纹理区域创建新的精灵将产生颜色映射器中定义的颜色值。
工作原理…
在开始之前,先简单介绍一下颜色知识;如果你在看这个食谱的代码,并对我们为条件语句和颜色结果选择的随机值感到困惑,这非常简单。每个颜色成分(红色、绿色和蓝色)可以提供 0 到 255 之间的任何颜色值。将 0 值传递给颜色成分将导致该颜色没有贡献,而传递 255 则被认为是完全颜色贡献。考虑到这一点,我们知道如果所有颜色成分返回 0 值,我们将把黑色传递给纹理区域的路径。如果我们给红色成分传递 255 值,同时绿色和蓝色都传递 0,我们知道纹理区域的路径将会是明亮的红色。
如果我们回顾一下如何操作...部分中的图表,我们可以看到alpha、红色、绿色和蓝色(ARGB)的颜色值,以及指向它们代表的圆圈区域的箭头。这些不会直接影响我们纹理区域颜色的最终结果;它们的存在仅仅是为了让我们可以在颜色映射器界面中引用圆圈的每一部分。注意,圆圈最外层的部分是明亮的红色,值为 255。考虑到这一点,请看我们颜色映射器中的以下条件:
// If the SVG image's color values equal red, or ARGB{0,255,0,0}
} else if(red == 255 && green == 0 && blue == 0){
// Return a pure blue color
return Color.argb(0, 0, 0, 255);
// If the SVG image's color values equal green, or ARGB{0,0,255,0}
}
前一段代码中的条件语句将会检查SVG
图像中是否包含没有任何绿色或蓝色贡献的纯红色值,并以纯蓝色替代。这就是颜色交换的原理,也是我们如何将颜色映射到图像中的方法!了解到这一点,我们完全有可能为我们的SVG
图像创建许多不同的颜色集合,但针对每一组颜色,我们必须提供一个独立的纹理区域。
需要特别注意的一个重要关键是,我们应该包含一个返回值,当我们的条件都不满足时,它会返回默认路径的颜色值。这允许我们省略一些条件,比如SVG
图像的轮廓或其他颜色等小细节,而是在我们喜欢的SVG
编辑器中打开图像时按出现的颜色填充。这应该作为颜色映射器中的最后一个else
语句包含:
// If our "custom color" conditionals do not apply...
} else {
// Return the SVG image's default color values
return Color.argb(alpha, red, green, blue);
}
还有更多…
在本食谱的工作原理...部分,我们介绍了如何改变静态SVG
图像路径的颜色。如果不深入考虑上述提到的创建颜色主题的想法,这听起来像是创建更多对象、地形、角色等的终极方法。但事实上,在当今时代,许多游戏需要变化以创造吸引人的资源。所谓的变化,当然是指渐变。回想我们上面编写的条件语句,我们在返回自定义颜色之前检查绝对的颜色值。
幸运的是,处理渐变并不太困难,因为我们可以调整渐变的停止颜色,而颜色之间的插值将自动为我们处理!我们可以将停止点视为定义渐变颜色的点,随着距离的增加,它在其他停止点之间进行插值。这就是产生渐变混合效果的原因,这也在通过本食谱中描述的相同方法创建颜色主题时发挥作用。以下是开始为纯红色RGB{255, 0, 0}
,到纯绿色RGB{0, 255, 0}
,最后到蓝色RGB{0, 0, 255}
的渐变的屏幕截图:
如果我们要在SVG
图像中使用上述渐变,只需简单修改每个停止点的特定颜色位置,就可以轻松应用颜色映射以及颜色停止点之间的适当插值。以下代码将改变渐变,使其呈现红色、绿色和黄色,而不是将蓝色作为第三个颜色停止点:
} else if(red == 0 && green == 0 && blue == 255){
// Return a pure blue color
return Color.argb(0, 255, 255, 0);
}
另请参阅…
- 使用 SVG 创建高分辨率图形部分。
第十章:从 AndEngine 获取更多内容
本章将介绍比前几章更具体应用的附加食谱。这些食谱包括:
-
从文件夹加载所有纹理
-
使用纹理网格
-
应用基于精灵的阴影
-
创建基于物理的移动平台
-
创建基于物理的绳索桥梁
从文件夹加载所有纹理
当创建一个包含大量纹理的游戏时,逐个加载每个纹理可能会变得繁琐。在这种游戏中创建加载和检索纹理的方法不仅可以节省开发时间,还可以减少运行时的整体加载时间。在本食谱中,我们将创建一种使用单行代码加载大量纹理的方法。
准备就绪...
首先,创建一个名为TextureFolderLoadingActivity
的新活动类,继承自BaseGameActivity
类。接下来,在assets/gfx/
文件夹中创建一个名为FolderToLoad
的文件夹。最后,将五张图片放入assets/gfx/FolderToLoad/
文件夹中,分别命名为:Coin1
、Coin5
、Coin10
、Coin50
和Coin100
。
如何操作...
按照以下步骤填写我们的TextureFolderLoadingActivity
活动类:
-
在我们的活动中放置以下简单的代码使其功能化:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
接下来,将这个
ArrayList
变量和ManagedStandardTexture
类放在活动内:public final ArrayList<ManagedStandardTexture> loadedTextures = new ArrayList<ManagedStandardTexture>(); public class ManagedStandardTexture { public ITextureRegion textureRegion; public String name; public ManagedStandardTexture(String pName, final ITextureRegion pTextureRegion) { name = pName; textureRegion = pTextureRegion; } public void removeFromMemory() { loadedTextures.remove(this); textureRegion.getTexture().unload(); textureRegion = null; name = null; } }
-
然后,将下面两个方法添加到活动类中,以便我们通过只传递
TextureOptions
参数和文件名来加载纹理:public ITextureRegion getTextureRegion(TextureOptions pTextureOptions, String pFilename) { loadAndManageTextureRegion(pTextureOptions,pFilename); return loadedTextures.get( loadedTextures.size()-1).textureRegion; } public void loadAndManageTextureRegion(TextureOptions pTextureOptions, String pFilename) { AssetBitmapTextureAtlasSource cSource = AssetBitmapTextureAtlasSource.create( this.getAssets(), pFilename); BitmapTextureAtlas TextureToLoad = new BitmapTextureAtlas(mEngine.getTextureManager(), cSource.getTextureWidth(), cSource.getTextureHeight(), pTextureOptions); TextureRegion TextureRegionToLoad = BitmapTextureAtlasTextureRegionFactory. createFromAsset(TextureToLoad, this, pFilename, 0, 0); TextureToLoad.load(); loadedTextures.add(new ManagedStandardTexture( pFilename.substring( pFilename.lastIndexOf("/")+1, pFilename.lastIndexOf(".")), TextureRegionToLoad)); }
-
现在,插入以下方法,允许我们加载单个或多个文件夹内的所有纹理:
public void loadAllTextureRegionsInFolders(TextureOptions pTextureOptions, String... pFolderPaths) { String[] listFileNames; String curFilePath; String curFileExtension; for (int i = 0; i < pFolderPaths.length; i++) try { listFileNames = this.getAssets(). list(pFolderPaths[i].substring(0, pFolderPaths[i].lastIndexOf("/"))); for (String fileName : listFileNames) { curFilePath = pFolderPaths[i].concat(fileName); curFileExtension = curFilePath.substring( curFilePath.lastIndexOf(".")); if(curFileExtension. equalsIgnoreCase(".png") || curFileExtension. equalsIgnoreCase(".bmp") || curFileExtension. equalsIgnoreCase(".jpg")) loadAndManageTextureRegion( pTextureOptions, curFilePath); } } catch (IOException e) { System.out.print("Failed to load textures from folder!"); e.printStackTrace(); return; } }
-
接着,将以下方法放入活动中,让我们可以卸载所有的
ManagedStandardTexture
类或通过其短文件名检索纹理:public void unloadAllTextures() { for(ManagedStandardTexture curTex : loadedTextures) { curTex.removeFromMemory(); curTex=null; loadedTextures.remove(curTex); } System.gc(); } public ITextureRegion getLoadedTextureRegion(String pName) { for(ManagedStandardTexture curTex : loadedTextures) if(curTex.name.equalsIgnoreCase(pName)) return curTex.textureRegion; return null; }
-
既然我们的活动类中已经有了所有方法,请在
onCreateResources()
方法中放置以下代码行:this.loadAllTextureRegionsInFolders(TextureOptions.BILINEAR, "gfx/FolderToLoad/");
-
最后,在
onPopulateScene()
方法中添加以下代码,以展示我们如何通过名称检索已加载的纹理:pScene.attachChild(new Sprite(144f, 240f, getLoadedTextureRegion("Coin1"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(272f, 240f, getLoadedTextureRegion("Coin5"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(400f, 240f, getLoadedTextureRegion("Coin10"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(528f, 240f, getLoadedTextureRegion("Coin50"), this.getVertexBufferObjectManager())); pScene.attachChild(new Sprite(656f, 240f, getLoadedTextureRegion("Coin100"), this.getVertexBufferObjectManager()));
工作原理...
在第一步中,我们通过实现大多数 AndEngine 游戏使用的标准覆盖BaseGameActivity
方法来设置我们的TextureFolderLoadingActivity
活动类。有关为 AndEngine 设置活动更多信息,请参见第一章中的了解生命周期食谱,AndEngine 游戏结构。
在第二步中,我们创建一个ManagedStandardTexture
对象的ArrayList
变量,这个定义紧跟在ArrayList
变量的定义之后。ManagedStandardTextures
是简单的容器,它持有一个指向ITextureRegion
区域的指针和一个表示ITextureRegion
对象名称的字符串变量。ManagedStandardTexture
类还包括一个卸载ITextureRegion
的方法,并准备在下次垃圾收集时从内存中移除这些变量。
第三步包括两个方法,getTextureRegion()
和loadAndManageTextureRegion()
:
-
getTextureRegion()
方法调用了loadAndManageTextureRegion()
方法,并从第二步中定义的名为loadedTextures
的ArrayList
变量返回最近加载的纹理。 -
loadAndManageTextureRegion()
方法创建了一个名为cSource
的AssetBitmapTextureAtlasSource
源,它仅用于在以下BitmapTextureAtlas
对象TextureToLoad
的定义中传递纹理的宽度和高度。
TextureRegion
对象TextureRegionToLoad
是通过调用BitmapTextureAtlasTextureRegionFactory
对象的createFromAsset()
方法创建的。然后加载TextureToLoad
,并通过创建一个新的ManagedStandardTexture
类,将TextureRegionToLoad
对象添加到loadedTextures
ArrayList
变量中。有关纹理的更多信息,请参见第一章中的不同类型的纹理食谱,AndEngine 游戏结构。
在第四步中,我们创建了一个方法,该方法解析通过pFolderPaths
数组传递的每个文件夹中的文件列表,并使用TextureOptions
参数将图像文件加载为纹理。listFileNames
字符串数组保存了pFolderPaths
文件夹中每个文件夹的文件列表,curFilePath
和curFileExtension
变量用于存储文件路径及其相对扩展名,以便确定哪些文件是 AndEngine 支持的图像。第一个for
循环简单地对每个给定的文件夹路径执行解析和加载过程。getAssets().list()
方法抛出IOException
异常,因此需要将其包含在try-catch
块中。它用于获取通过传递的String
参数中的所有文件列表。第二个for
循环将curFilePath
设置为当前i
值的文件夹路径与listFileNames
数组中的当前文件名拼接而成。接下来,curFileExtension
字符串变量被设置为curFilePath
变量的最后一个"。"索引,以返回扩展名,使用substring()
方法。然后,我们检查以确保当前文件的扩展名等于 AndEngine 支持的扩展名,并在为true
时调用loadAndManageTextureRegion()
方法。最后,我们通过向日志发送消息并打印来自IOException
异常的StackTrace
消息来捕获IOException
异常。
第五步包括两个方法,unloadAllTextures()
和getLoadedTextureRegion()
,它们协助我们管理通过我们之前的方法加载的纹理:
-
unloadAllTextures()
方法遍历loadedTextures
ArrayList
对象中的所有ManagedStandardTextures
,并使用removeFromMemory()
方法卸载它们,在从loadedTextures
中移除它们并请求系统进行垃圾回收之前。 -
getLoadedTextureRegion()
方法检查loadedTextures
变量中的每个ManagedStandardTexture
,与pName
字符串参数进行对比,如果名称相等,则返回当前ManagedStandardTexture
类的ITextureRegion
区域,否则如果没有匹配,则返回null
。
第六步通过传递一个BILINEAR
TextureOption
参数和我们的FolderToLoad
文件夹的资产文件夹路径,从onCreateResources()
活动方法内部调用loadAllTextureRegionsInFolders()
方法。有关TextureOptions
的更多信息,请参见第一章,AndEngine 游戏结构中的向我们的纹理应用选项食谱。
在最后一步中,我们在onPopulateScene()
活动方法内部将五个精灵附加到我们的场景中。每个精灵构造函数都调用getLoadedTextureRegion()
方法,并传递精灵图像文件的相应简称。每个精灵的位置将它们放置在屏幕上的一条水平线上。一次性加载纹理的精灵显示应类似于以下图像。有关创建精灵的更多信息,请参见第二章,使用实体中的向层中添加精灵食谱。
另请参阅
-
在第一章,AndEngine 游戏结构中的理解生命周期。
-
在第一章,AndEngine 游戏结构中的不同类型的纹理。
-
在第一章,AndEngine 游戏结构中的向我们的纹理应用选项。
-
在第二章,使用实体中的向层中添加精灵。
使用纹理网格
纹理网格,即简单应用了纹理的三角剖分多边形,在移动游戏中越来越受欢迎,因为它们允许创建和非矩形形状的操作。具有处理纹理网格的能力通常创建了一个额外的游戏机制层,这些机制以前实现起来成本过高。在本食谱中,我们将学习如何从一组预定的三角形创建纹理网格。
准备就绪...
首先,创建一个名为TexturedMeshActivity
的新活动类,继承自BaseGameActivity
。接下来,将一个名为dirt.png
的无缝拼接纹理,尺寸为 512 x 128,放在我们项目的assets/gfx/
文件夹中。最后,将代码包中的TexturedMesh.java
类导入到我们的项目中。
如何操作...
按照以下步骤构建我们的TexturedMeshActivity
活动类:
-
在我们的活动中放置以下代码,以获得一个标准的 AndEngine 活动:
@Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
在
onPopulateScene()
方法中添加以下代码片段:BitmapTextureAtlas texturedMeshT = new BitmapTextureAtlas( this.getTextureManager(), 512, 128, TextureOptions.REPEATING_BILINEAR); ITextureRegion texturedMeshTR = BitmapTextureAtlasTextureRegionFactory. createFromAsset(texturedMeshT, this, "gfx/dirt.png", 0, 0); texturedMeshT.load(); float[] meshTriangleVertices = { 24.633111f,37.7835047f,-0.00898f,113.0324447f, -24.610162f,37.7835047f,0.00387f,-37.7900953f, -103.56176f,37.7901047f,103.56176f,37.7795047f, 0.00387f,-37.7900953f,-39.814736f,-8.7311953f, -64.007044f,-83.9561953f,64.00771f,-83.9621953f, 39.862562f,-8.7038953f,0.00387f,-37.7900953f}; float[] meshBufferData = new float[TexturedMesh.VERTEX_SIZE * (meshTriangleVertices.length/2)]; for( int i = 0; i < meshTriangleVertices.length/2; i++) { meshBufferData[(i * TexturedMesh.VERTEX_SIZE) + TexturedMesh.VERTEX_INDEX_X] = meshTriangleVertices[i*2]; meshBufferData[(i * TexturedMesh.VERTEX_SIZE) + TexturedMesh.VERTEX_INDEX_Y] = meshTriangleVertices[i*2+1]; } TexturedMesh starTexturedMesh = new TexturedMesh(400f, 225f, meshBufferData, 12, DrawMode.TRIANGLES, texturedMeshTR, this.getVertexBufferObjectManager()); pScene.attachChild(starTexturedMesh);
工作原理...
在第一步中,我们准备TexturedMeshActivity
类,通过插入大多数 AndEngine 游戏使用的标准的重写BaseGameActivity
方法。有关使用 AndEngine 设置活动的更多信息,请参见第一章,Understanding the life cycle部分。
在第二步中,我们首先定义了texturedMeshT
,这是一个BitmapTextureAtlas
对象,构造函数的最后一个参数是REPEATING_BILINEAR
TextureOption
,用于创建一个在构成我们纹理网格的三角形中无缝平铺的纹理。有关TextureOptions
的更多信息,请参见第一章,Applying options to our textures部分。
创建了texturedMeshTR
ITextureRegion
对象并加载了我们的texturedMeshT
对象之后,我们定义了一个浮点数数组,用于指定构成我们纹理网格的每个三角形的每个顶点的相对连续的 x 和 y 位置。以下图片将更好地展示如何在纹理网格中使用三角形的顶点:
接下来,我们创建meshBufferData
浮点数组,并将其大小设置为TexturedMesh
类的顶点大小乘以meshTriangleVertices
数组中的顶点数——一个顶点在数组中占用两个索引,X
和Y
,因此我们必须将长度除以2
。然后,对于meshTriangleVertices
数组中的每个顶点,我们将顶点的位置应用到meshBufferData
数组中。最后,我们创建名为starTexturedMesh
的TexturedMesh
对象。TexturedMesh
构造函数的参数如下:
-
构造函数的前两个参数是
400f
,225f
的 x 和 y 位置。 -
接下来的两个参数是
meshBufferData
缓冲数据和我们在meshBufferData
数组中放置的顶点数,12
。 -
TexturedMesh
构造函数的最后三个参数是Triangles
的DrawMode
、网格的ITextureRegion
和我们VertexBufferObjectManager
对象。
有关创建Meshes
的更多信息,从中派生出TexturedMesh
类,请参见第二章,Applying primitives to a layer部分。
参见以下内容
-
在第一章,AndEngine 游戏结构中,了解生命周期,即Understanding the life cycle。
-
在第一章,AndEngine 游戏结构中,我们讨论了如何将选项应用到我们的纹理中,即Applying options to our textures。
-
在第二章,Working with Entities中,我们讨论了如何将图元应用到图层,即Applying primitives to a layer。
应用基于精灵的阴影
在游戏中添加阴影可以增加视觉深度,使游戏更具吸引力。简单地在对象下方放置一个带有阴影纹理的精灵是一种快速有效的处理阴影创建的方法。在本章中,我们将学习如何保持阴影与其父对象正确对齐的同时完成这一工作。
准备就绪...
首先,创建一个名为SpriteShadowActivity
的新活动类,该类继承自BaseGameActivity
并实现IOnSceneTouchListener
。接下来,将大小为 256 x 128 且名为shadow.png
的阴影图像放入assets/gfx/
文件夹中。最后,将大小为 128 x 256 且名为character.png
的角色图像放入assets/gfx/
文件夹中。
如何操作...
按照以下步骤构建我们的SpriteShadowActivity
活动类:
-
在我们的活动类中放入以下标准的 AndEngine 活动代码:
@Override public EngineOptions onCreateEngineOptions() { EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); engineOptions.getRenderOptions().setDithering(true); return engineOptions; } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.8f,0.8f,0.8f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pScene.setOnSceneTouchListener(this); pOnPopulateSceneCallback.onPopulateSceneFinished(); } @Override public boolean onSceneTouchEvent(Scene pScene, TouchEvent pSceneTouchEvent) { return true; }
-
接下来,在我们的活动中放置这些变量,以便我们具体控制阴影:
Static final float CHARACTER_START_X = 400f; static final float CHARACTER_START_Y = 128f; static final float SHADOW_OFFSET_X = 0f; static final float SHADOW_OFFSET_Y = -64f; static final float SHADOW_MAX_ALPHA = 0.75f; static final float SHADOW_MIN_ALPHA = 0.1f; static final float SHADOW_MAX_ALPHA_HEIGHT = 200f; static final float SHADOW_MIN_ALPHA_HEIGHT = 0f; static final float SHADOW_START_X = CHARACTER_START_X + SHADOW_OFFSET_X; static final float SHADOW_START_Y = CHARACTER_START_Y + SHADOW_OFFSET_Y; static final float CHARACTER_SHADOW_Y_DIFFERENCE = CHARACTER_START_Y - SHADOW_START_Y; static final float SHADOW_ALPHA_HEIGHT_DIFFERENCE = SHADOW_MAX_ALPHA_HEIGHT - SHADOW_MIN_ALPHA_HEIGHT; static final float SHADOW_ALPHA_DIFFERENCE = SHADOW_MAX_ALPHA - SHADOW_MIN_ALPHA; Sprite shadowSprite; Sprite characterSprite;
-
现在,将以下方法放入我们的活动中,使阴影的 alpha 值与角色与阴影的距离成反比:
public void updateShadowAlpha() { shadowSprite.setAlpha(MathUtils.bringToBounds( SHADOW_MIN_ALPHA, SHADOW_MAX_ALPHA, SHADOW_MAX_ALPHA - ((((characterSprite.getY()- CHARACTER_SHADOW_Y_DIFFERENCE)-SHADOW_START_Y) / SHADOW_ALPHA_HEIGHT_DIFFERENCE) * SHADOW_ALPHA_DIFFERENCE))); }
-
在
onSceneTouchEvent()
方法中插入以下代码片段:if(pSceneTouchEvent.isActionDown() || pSceneTouchEvent.isActionMove()) { characterSprite.setPosition( pSceneTouchEvent.getX(), Math.max(pSceneTouchEvent.getY(), CHARACTER_START_Y)); }
-
最后,用以下代码片段填充
onPopulateScene()
方法:BitmapTextureAtlas characterTexture = new BitmapTextureAtlas(this.getTextureManager(), 128, 256, TextureOptions.BILINEAR); TextureRegion characterTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset( characterTexture, this, "gfx/character.png", 0, 0); characterTexture.load(); BitmapTextureAtlas shadowTexture = new BitmapTextureAtlas(this.getTextureManager(), 256, 128, TextureOptions.BILINEAR); TextureRegion shadowTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset( shadowTexture, this, "gfx/shadow.png", 0, 0); shadowTexture.load(); shadowSprite = new Sprite(SHADOW_START_X, SHADOW_START_Y, shadowTextureRegion,this.getVertexBufferObjectManager()); characterSprite = new Sprite(CHARACTER_START_X, CHARACTER_START_Y, characterTextureRegion,this.getVertexBufferObjectManager()) { @Override public void setPosition(final float pX, final float pY) { super.setPosition(pX, pY); shadowSprite.setPosition( pX + SHADOW_OFFSET_X, shadowSprite.getY()); updateShadowAlpha(); } }; pScene.attachChild(shadowSprite); pScene.attachChild(characterSprite); updateShadowAlpha();
它是如何工作的...
在第一步中,我们通过实现大多数 AndEngine 游戏使用的标准覆盖BaseGameActivity
方法来设置我们的SpriteShadowActivity
活动类。有关使用 AndEngine 设置活动的更多信息,请参见第一章中的了解生命周期部分,AndEngine 游戏结构。
下图展示了这个方法是如何将我们的阴影精灵放置在角色精灵的关系位置上的:
在第二步中,我们定义了几个常量,这些常量将控制阴影精灵shadowSprite
与角色精灵characterSprite
的对齐方式:
-
前两个常量
CHARACTER_START_X
和CHARACTER_START_Y
设置了characterSprite
的初始位置。 -
接下来的两个常量
SHADOW_OFFSET_X
和SHADOW_OFFSET_Y
控制了阴影与角色精灵在 x 和 y 轴上的初始位置距离。 -
SHADOW_OFFSET_X
常量也用于在移动角色精灵时更新阴影精灵的位置。
接下来的四个常量控制了shadowSprite
精灵的 alpha 值如何被控制以及控制到什么程度:
-
SHADOW_MAX_ALPHA
和SHADOW_MIN_ALPHA
设置了 alpha 值的绝对最大和最小值,这会根据角色与阴影在 y 轴上的距离而改变。距离越远,shadowSprite
的 alpha 值越低,直至达到最低水平。 -
SHADOW_MAX_ALPHA_HEIGHT
常量表示角色与阴影的距离在影响shadowSprite
的 alpha 值之前,可以达到的最大距离,之后默认为SHADOW_MIN_ALPHA
。 -
SHADOW_MIN_ALPHA_HEIGHT
常量表示角色距离阴影的最小距离,该距离会影响阴影的透明度变化。如果SHADOW_MIN_ALPHA_HEIGHT
大于0
,当角色距离阴影低于SHADOW_MIN_ALPHA_HEIGHT
时,阴影的透明度将处于最大值。
剩余的常量会从之前的集合中自动计算得出。SHADOW_START_X
和 SHADOW_START_Y
代表 shadowSprite
图像的起始位置。它们是通过将阴影的偏移值加到角色的起始位置来计算的。CHARACTER_SHADOW_Y_DIFFERENCE
常量表示角色与阴影在 y 轴上的初始起始距离。SHADOW_ALPHA_HEIGHT_DIFFERENCE
常量表示最小高度和最大高度之间的差,用于在运行时调节阴影的透明度。最后的常量 SHADOW_ALPHA_DIFFERENCE
表示 shadowSprite
图像的最小和最大透明度水平之间的差。与 SHADOW_ALPHA_HEIGHT_DIFFERENCE
常量类似,它在运行时用于确定阴影的透明度水平。
在第二步中的最后两个变量 shadowSprite
和 characterSprite
分别代表我们场景中的阴影和角色。
在第三步中,我们创建一个方法来更新阴影的透明度。我们调用 shadowSprite.setAlpha()
方法,并以 MathUtils.bringToBounds()
方法作为参数。MathUtils.bringToBounds()
方法接受一个最小值和最大值,确保第三个值在这个范围内。我们将 SHADOW_MIN_ALPHA
和 SHADOW_MAX_ALPHA
常量作为 bringToBounds()
方法的头两个参数传递。
第三个参数是基于 characterSprite
图像与 shadowSprite
图像之间的距离确定阴影透明度的算法。该算法首先从角色的 y 轴位置减去 CHARACTER_SHADOW_Y_DIFFERENCE
常量。这为我们提供了当前影响阴影透明度的 y 值的上限。接下来,我们从 y 轴上的阴影起始位置减去该值,以得到当前角色与阴影的理想距离。然后,我们将该距离除以 SHADOW_ALPHA_HEIGHT_DIFFERENCE
,以得到约束距离到透明度的单位比率,并将该比率乘以 SHADOW_ALPHA_DIFFERENCE
常量,以得到约束距离到约束透明度的单位比率。目前,我们的比率是倒置的,随着距离的增加会提高透明度,这与我们随着角色移动更远而降低透明度的目标相反,因此我们从 SHADOW_MAX_ALPHA
常量中减去它,以得到随着距离增加而降低透明度的正确比率。完成算法后,我们使用 bringToBounds()
方法确保算法产生的透明度值被限制在 SHADOW_MIN_ALPHA
到 SHADOW_MAX_ALPHA
的范围内。
第四步通过检查触摸事件的 isActionDown()
和 isActionMove()
属性,设置在屏幕首次触摸或触摸移动时 characterSprite
精灵的位置。在这种情况下,setPosition()
方法简单地将 x 值设置为触摸的 x 值,将 y 值设置为触摸的 y 值或角色的起始 y 值,以较大者为准。
在最后一步中,我们加载 TextureRegions
、characterTextureRegion
和 shadowTextureRegion
对象,用于角色和阴影。关于 TextureRegions
的更多信息,请参见第一章,AndEngine 游戏结构中的不同类型的纹理食谱。然后,我们使用它们的起始常量作为构造函数中的位置创建 shadowSprite
和 characterSprite
精灵。对于 characterSprite
,我们重写 setPosition()
方法,也设置偏移 x 后的 shadowSprite
精灵的位置,然后调用 updateShadowAlpha()
方法,以在角色移动后为阴影设置适当的 alpha 值。最后,我们将 shadowSprite
和 characterSprite
精灵附加到我们的场景中,并调用 updateShadowAlpha()
方法设置阴影的初始 alpha 值。以下图片显示了阴影的 alpha 级别如何相对于角色距离的变化而改变:
另请参阅
-
在第一章,AndEngine 游戏结构中了解生命周期。
-
在第一章,AndEngine 游戏结构中了解不同类型的纹理。
创建基于物理的移动平台
大多数平台风格的游戏都有某种移动平台,这挑战玩家以准确的时机着陆。从开发者的角度来看,平台只是一个从一处移动到另一处的物理启用的物体。在本教程中,我们将了解如何创建一个水平移动的平台。
准备就绪...
创建一个名为 MovingPhysicsPlatformActivity
的新活动类,该类继承自 BaseGameActivity
。
如何操作...
按照以下步骤构建我们的 MovingPhysicsPlatformActivity
活动类:
-
在我们的活动中插入以下代码段以使其功能正常:
@Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480) ).setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
在
onPopulateScene()
方法中添加以下代码段:FixedStepPhysicsWorld mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0,-SensorManager.GRAVITY_EARTH*2f), false, 8, 3); pScene.registerUpdateHandler(mPhysicsWorld); Rectangle platformRect = new Rectangle(400f, 200f, 250f, 20f, this.getVertexBufferObjectManager()); platformRect.setColor(0f, 0f, 0f); final FixtureDef platformFixtureDef = PhysicsFactory.createFixtureDef(20f, 0f, 1f); final Body platformBody = PhysicsFactory.createBoxBody( mPhysicsWorld, platformRect, BodyType.KinematicBody, platformFixtureDef); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(platformRect, platformBody)); pScene.attachChild(platformRect); float platformRelativeMinX = -200f; float platformRelativeMaxX = 200f; final float platformVelocity = 3f; final float platformMinXWorldCoords = (platformRect.getX() + platformRelativeMinX) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; final float platformMaxXWorldCoords = (platformRect.getX() + platformRelativeMaxX) / PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT; platformBody.setLinearVelocity(platformVelocity, 0f);
-
在
onPopulateScene()
方法中的前一行代码下面直接插入以下代码:pScene.registerUpdateHandler(new IUpdateHandler() { @Override public void onUpdate(float pSecondsElapsed) { if(platformBody.getWorldCenter().x > platformMaxXWorldCoords) { platformBody.setTransform( platformMaxXWorldCoords, platformBody.getWorldCenter().y, platformBody.getAngle()); platformBody.setLinearVelocity( -platformVelocity, 0f); } else if(platformBody.getWorldCenter().x < platformMinXWorldCoords) { platformBody.setTransform( platformMinXWorldCoords, platformBody.getWorldCenter().y, platformBody.getAngle()); platformBody.setLinearVelocity( platformVelocity, 0f); } } @Override public void reset() {} });
-
在
onPopulateScene()
方法中完成我们的活动,通过在前一行代码之后放置以下代码来创建一个在平台上休息的物理启用的盒子:Rectangle boxRect = new Rectangle(400f, 240f, 60f, 60f, this.getVertexBufferObjectManager()); boxRect.setColor(0.2f, 0.2f, 0.2f); FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(200f, 0f, 1f); mPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(boxRect, PhysicsFactory.createBoxBody( mPhysicsWorld, boxRect, BodyType.DynamicBody, boxFixtureDef))); pScene.attachChild(boxRect);
工作原理...
在第一步中,我们准备MovingPhysicsPlatformActivity
类,通过向其中插入大多数 AndEngine 游戏使用的标准覆盖BaseGameActivity
方法。关于如何为 AndEngine 设置活动的更多信息,请参见第一章中的了解生命周期一节,AndEngine 游戏结构。以下图片展示了我们的平台如何在单轴上移动,在本例中是向右移动,同时保持上面的盒子:
在第二步中,我们首先创建一个FixedStepPhysicsWorld
对象,并将其注册为场景的更新处理器。然后,我们创建一个名为platformRect
的Rectangle
对象,它将代表我们的移动平台,并将其放置在屏幕中心附近。接下来,我们使用setColor()
方法将platformRect
矩形的颜色设置为黑色,红色、绿色和蓝色的浮点参数值为0f
。然后,我们为平台创建一个固定装置定义。注意,摩擦力设置为1f
,以防止物体在平台移动时滑动过多。
接下来,我们为平台创建一个名为platformBody
的Body
对象。然后,我们注册一个PhysicsConnector
类,将platformRect
矩形连接到platformBody
对象。将platformRect
附加到我们的场景后,我们声明并设置将控制移动平台的变量:
-
platformRelativeMinX
和platformRelativeMaxX
变量表示平台从其起始位置向左和向右移动的场景单位距离。 -
platformVelocity
变量表示我们物理平台物体的速度,单位为每秒米。 -
接下来的两个变量
platformMinXWorldCoords
和platformMaxXWorldCoords
表示platformRelativeMinX
和platformRelativeMaxX
变量的绝对位置,并从平台的初始 x 位置按默认的PIXEL_TO_METER_RATIO_DEFAULT
比例计算得出。 -
最后,我们将
platformBody
的初始速度设置为platformVelocity
变量,以使物体在场景首次绘制时立即主动移动。关于创建物理模拟的更多信息,请参见第六章中的Box2D 物理扩展介绍和了解不同的物体类型一节。
第三步,我们向场景注册一个新的IUpdateHandler
处理器。在onUpdate()
方法中,我们测试平台的位置是否超出了之前定义的绝对边界platformMinXWorldCoords
和platformMaxXWorldCoords
。根据达到的绝对边界,我们将platformBody
的位置设置到达到的边界,并将其速度设置为远离边界。关于条件更新处理器的更多信息,请参见第七章中的更新处理器与条件部分。
在第四步中,我们创建并附加一个盒子物体,使其在平台上休息。关于如何创建具有物理效果的盒子,请参考第六章中的了解不同的物体类型部分。
另请参阅
-
在第一章中了解生命周期。
-
在第六章中查看Box2D 物理扩展介绍。
-
在第六章中了解不同的物体类型。
-
在第七章中查看更新处理器与条件。
创建基于物理的绳索桥梁
使用 Box2D 物理扩展,创建复杂的物理效果元素很简单。一个这样的复杂元素例子就是能对碰撞做出反应的绳索桥梁。在本教程中,我们将看到如何实现一个根据特定参数创建绳索桥梁的方法,这些参数控制着桥梁的大小和物理属性。
准备工作...
创建一个名为PhysicsBridgeActivity
的新活动类,该类继承自BaseGameActivity
。
如何操作...
按照以下步骤构建我们的PhysicsBridgeActivity
活动类:
-
在我们的活动中放置以下代码,以获得标准的 AndEngine 活动:
@Override public Engine onCreateEngine(final EngineOptions pEngineOptions) { return new FixedStepEngine(pEngineOptions, 60); } @Override public EngineOptions onCreateEngineOptions() { return new EngineOptions(true, ScreenOrientation.LANDSCAPE_SENSOR, new FillResolutionPolicy(), new Camera(0, 0, 800, 480)) .setWakeLockOptions(WakeLockOptions.SCREEN_ON); } @Override public void onCreateResources(OnCreateResourcesCallback pOnCreateResourcesCallback) { pOnCreateResourcesCallback.onCreateResourcesFinished(); } @Override public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) { Scene mScene = new Scene(); mScene.setBackground(new Background(0.9f,0.9f,0.9f)); pOnCreateSceneCallback.onCreateSceneFinished(mScene); } @Override public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) { pOnPopulateSceneCallback.onPopulateSceneFinished(); }
-
接下来,在我们的活动中放置以下不完整的方法。这个方法将有助于我们创建桥梁:
public void createBridge(Body pGroundBody, final float[] pLeftHingeAnchorPoint, final float pRightHingeAnchorPointX, final int pNumSegments, final float pSegmentsWidth, final float pSegmentsHeight, final float pSegmentDensity, final float pSegmentElasticity, final float pSegmentFriction, IEntity pScene, PhysicsWorld pPhysicsWorld, VertexBufferObjectManager pVertexBufferObjectManager) { final Rectangle[] BridgeSegments = new Rectangle[pNumSegments]; final Body[] BridgeSegmentsBodies = new Body[pNumSegments]; final FixtureDef BridgeSegmentFixtureDef = PhysicsFactory.createFixtureDef( pSegmentDensity, pSegmentElasticity, pSegmentFriction); final float BridgeWidthConstant = pRightHingeAnchorPointX – pLeftHingeAnchorPoint[0] + pSegmentsWidth; final float BridgeSegmentSpacing = ( BridgeWidthConstant / (pNumSegments+1) – pSegmentsWidth/2f); for(int i = 0; i < pNumSegments; i++) { } }
-
在上述
createBridge()
方法中的for
循环内插入以下代码:BridgeSegments[i] = new Rectangle( ((BridgeWidthConstant / (pNumSegments+1))*i) + pLeftHingeAnchorPoint[0] + BridgeSegmentSpacing, pLeftHingeAnchorPoint[1]-pSegmentsHeight/2f, pSegmentsWidth, pSegmentsHeight, pVertexBufferObjectManager); BridgeSegments[i].setColor(0.97f, 0.75f, 0.54f); pScene.attachChild(BridgeSegments[i]); BridgeSegmentsBodies[i] = PhysicsFactory.createBoxBody( pPhysicsWorld, BridgeSegments[i], BodyType.DynamicBody, BridgeSegmentFixtureDef); BridgeSegmentsBodies[i].setLinearDamping(1f); pPhysicsWorld.registerPhysicsConnector( new PhysicsConnector(BridgeSegments[i], BridgeSegmentsBodies[i])); final RevoluteJointDef revoluteJointDef = new RevoluteJointDef(); if(i==0) { Vector2 anchorPoint = new Vector2( BridgeSegmentsBodies[i].getWorldCenter().x – (BridgeSegmentSpacing/2 + pSegmentsWidth/2)/ PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(pGroundBody, BridgeSegmentsBodies[i], anchorPoint); } else { Vector2 anchorPoint = new Vector2( (BridgeSegmentsBodies[i].getWorldCenter().x + BridgeSegmentsBodies[i-1] .getWorldCenter().x)/2, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(BridgeSegmentsBodies[i-1], BridgeSegmentsBodies[i], anchorPoint); } pPhysicsWorld.createJoint(revoluteJointDef); if(i==pNumSegments-1) { Vector2 anchorPoint = new Vector2(BridgeSegmentsBodies[i].getWorldCenter().x + (BridgeSegmentSpacing/2 + pSegmentsWidth/2)/PhysicsConstants.PIXEL_TO_METER_RATIO_DEFAULT, BridgeSegmentsBodies[i].getWorldCenter().y); revoluteJointDef.initialize(pGroundBody, BridgeSegmentsBodies[i], anchorPoint); pPhysicsWorld.createJoint(revoluteJointDef); }
-
最后,在我们的
onPopulateScene()
方法内添加以下代码:final FixedStepPhysicsWorld mPhysicsWorld = new FixedStepPhysicsWorld(60, new Vector2(0,-SensorManager.GRAVITY_EARTH), false, 8, 3); pScene.registerUpdateHandler(mPhysicsWorld); FixtureDef groundFixtureDef = PhysicsFactory.createFixtureDef(0f, 0f, 0f); Body groundBody = PhysicsFactory.createBoxBody(mPhysicsWorld, 0f, 0f, 0f, 0f, BodyType.StaticBody, groundFixtureDef); createBridge(groundBody, new float[] {0f,240f}, 800f, 16, 40f, 10f, 4f, 0.1f, 0.5f, pScene, mPhysicsWorld, this.getVertexBufferObjectManager()); Rectangle boxRect = new Rectangle(100f,400f,50f,50f,this.getVertexBufferObjectManager()); FixtureDef boxFixtureDef = PhysicsFactory.createFixtureDef(25f, 0.5f, 0.5f); mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector(boxRect, PhysicsFactory.createBoxBody(mPhysicsWorld, boxRect, BodyType.DynamicBody, boxFixtureDef))); pScene.attachChild(boxRect);
工作原理...
在第一步中,我们通过实现大多数 AndEngine 游戏使用的标准覆盖BaseGameActivity
方法来设置PhysicsBridgeActivity
活动类。关于如何为 AndEngine 设置活动,请参考第一章中的了解生命周期部分。以下图片展示了我们带有物理效果的桥梁,以及一个带有物理效果的方块在其上休息的样子:
在第二步中,我们实现了一个名为createBridge()
的方法的开头,该方法将创建具有物理效果的桥。第一个参数pGroundBody
是桥将附加到的地面Body
对象。第二个参数pLeftHingeAnchorPoint
表示桥左上侧的 x 和 y 位置。第三个参数pRightHingeAnchorPointX
表示桥右侧的 x 位置。接下来的三个参数pNumSegments
、pSegmentsWidth
和pSegmentsHeight
表示桥将由多少个桥段组成以及每个桥段的宽度和高度。pSegmentDensity
、pSegmentElasticity
和pSegmentFriction
参数将直接传递给一个夹具定义,该定义将应用于桥的桥段。有关夹具定义的更多信息,请参见第六章,物理应用中的Box2D 物理扩展介绍食谱。接下来的两个参数pScene
和pPhysicsWorld
告诉我们的方法桥段矩形和桥段实体应该附加到什么上。最后一个参数是我们的VertexBufferObjectManager
对象,它将被传递给表示我们桥每个段的矩形。
在createBridge()
方法中定义的前两个变量,BridgeSegments
和BridgeSegmentsBodies
,是用于保存桥段矩形和桥段实体的数组。它们的长度由pNumSegments
参数传递定义。下一个变量,BridgeSegmentFixtureDef
,是每个桥段将拥有的夹具定义。BridgeWidthConstant
变量表示桥的宽度,通过计算左侧和右侧锚点加上桥的单个桥段宽度之差得出。最后一个变量,BridgeSegmentSpacing
,表示每个桥段之间应有的空间,通过将桥的宽度除以桥段数量加一,然后减去桥段半宽度得出。然后我们创建一个for
循环,该循环将根据pNumSegments
参数传递的数量创建并定位桥段。
在第三步中,我们填充之前创建的for
循环。首先,我们创建当前桥段的矩形BridgeSegments[i]
,它将作为桥段的视觉表示。我们将其放置在 x 轴上,使用BridgeWidthConstant
变量除以桥段数量加一,然后乘以当前桥段编号,并加上左侧铰链的 x 位置pLeftHingeAnchorPoint[0]
和桥段之间的间距BridgeSegmentSpacing
。对于当前桥段矩形的 y 轴位置,我们将其放置在左侧铰链的 y 位置减去桥段高度除以2f
的位置,使其与铰链位置平齐。
接下来,我们将每个段落的颜色设置为浅橙色,红色0.97f
,绿色0.75f
,蓝色0.54f
。将Rectangle
对象附加到传递的场景后,通过将段落的矩形和BodyType
值Dynamic
传递给标准的PhysicsFactory.CreateBoxBody()
方法来创建当前段落的刚体。然后,我们将线性阻尼设置为1f
,以平滑由碰撞引起的节奏性运动。接下来,我们注册一个PhysicsConnector
类,将当前段落的矩形连接到当前段落的刚体。
既然我们已经为每个段落建立了位置并创建了相应的矩形和刚体,我们创建一个RevoluteJointDef
对象revoluteJointDef
,通过旋转关节将每个段落连接到桥梁。我们测试当前段落是否是第一个,如果是,则将段落连接到地面Body
对象,而不是前一个段落。对于第一个桥梁段落,Vector2 anchorPoint
的定义将RevoluteJointDef
定义的锚点放置在段落的 x 值BridgeSegmentsBodies[i].getWorldCenter().x
减去段落间距BridgeSegmentSpacing
除以2
,加上段落宽度pSegmentsWidth
除以2
,并缩放到PIXEL_TO_METER_RATIO_DEFAULT
默认值的位置。第一个段落锚点的 y 位置简单地是当前段落的 y 值BridgeSegmentsBodies[i].getWorldCenter().y
。对于其余的段落,通过计算当前段落的 x 位置与上一个段落的 x 位置的均值来确定锚点的 x 位置。
然后,使用initialize()
方法初始化revoluteJointDef
,第一个刚体设置为地面刚体pGroundBody
,如果当前段落是第一个;如果不是第一个,则设置为前一段的刚体BridgeSegmentsBodies[i-1]
。revoluteJointDef
的第二个刚体设置为当前段落的刚体,并在退出if
语句后,使用pPhysicsWorld
对象的createJoint()
方法创建关节。然后我们测试当前段落是否将是最后一个创建的,如果是,则使用与第一个段落相似的锚点 x 位置公式,在段落的右侧创建另一个旋转关节,将段落连接到地面刚体。有关物理模拟的更多信息,请参见第六章,物理应用中的Box2D 物理扩展介绍和了解不同的刚体类型食谱。
在最后一步中,我们首先在onPopulateScene()
方法内部创建一个FixedStepPhysicsWorld
对象,并将其注册为场景的更新处理器。然后,我们创建一个地面物体,我们的桥梁将附着在上面。接下来,我们通过调用createBridge()
方法来创建桥梁。我们传递groundBody
作为第一个参数,一个表示屏幕左中部的位置0f,240f
作为左锚点,以及代表屏幕右侧的 x 位置作为右锚点。然后,我们传递一个整数16
作为要创建的段数,以及一个段宽和高度为40f
和10f
。接下来,我们传递一个段密度4f
,一个段弹性0.1f
,一个段摩擦0.5f
,我们的场景,将段矩形将附着其上,我们的物理世界,以及我们的VertexBufferObjectManager
对象。现在我们的桥梁已经创建好了,我们创建了一个简单的盒子物体,以显示桥梁能够正确地反应碰撞。
另请参阅
-
在第一章,AndEngine 游戏结构中了解生命周期。
-
在第六章,应用物理中介绍Box2D 物理扩展。
-
在第六章,应用物理中理解不同的物体类型。
附录 A. MagneTank 的源代码
本章为游戏MagneTank中使用的所有类别提供了简短的描述和参考资料。MagneTank 可在谷歌 Play 商店(play.google.com/store/apps/details?id=ifl.games.MagneTank
)上找到,以前称为Android Market,本书代码捆绑包中可以找到源代码。游戏玩法包括通过触摸炮塔应该指向的位置来瞄准坦克的炮塔,并在同一位置轻敲以发射炮塔。为了展示物理启用的车辆,可以通过首先触摸坦克,然后向所需方向滑动,将坦克拉到左侧或右侧。
游戏的类别分布在以下主题中:
-
游戏关卡类别
-
输入类别
-
图层类别
-
管理类别
-
菜单类别
-
活动和引擎类别
以下图片是 MagneTank 第二关的游戏内截图:
游戏关卡类别
这些类别出现在游戏的可玩部分:
ManagedGameScene.java
MagneTank 的ManagedGameScene
类别在第五章,场景和图层管理中呈现的ManagedGameScene
类别的基础上,通过添加分步加载屏幕来显示每个关卡加载的内容。使用加载步骤背后的想法与在加载游戏之前显示一帧加载屏幕类似,就像SceneManager
类别在显示新场景时的功能一样,但是加载屏幕会在每个加载步骤更新,而不仅仅是第一次显示加载屏幕时更新一次。
这个类别基于以下配方:
-
在第二章,使用实体中将文本应用于图层
-
在第五章,场景和图层管理中创建场景管理器
-
在第七章,使用更新处理器中更新处理器是什么?
GameLevel.java
GameLevel
类别将所有其他游戏内类别汇集在一起,形成了 MagneTank 的可玩部分。它处理每个实际游戏关卡的构建和执行。它扩展了一个自定义的ManagedGameScene
类别,该类别包含一系列LoadingRunnable
对象,这些对象分步骤创建关卡,允许关卡构建的每个进度在屏幕上显示。GameLevel
类别还使用GameManager
类别来确定每个游戏关卡的完成或失败,以测试胜利或失败条件。
这个类别基于以下配方:
-
在第二章,使用实体中了解 AndEngine 实体
-
在第二章中,处理实体一节讲述了使用精灵使场景生动。
-
在第二章中,处理实体一节介绍了给图层应用文本。
-
在第二章中,处理实体一节介绍了重写 onManagedUpdate 方法。
-
在第二章中,处理实体一节讲解了使用修改器和实体修改器。
-
在第三章中,设计你的菜单一节解释了使用视差背景创造透视感。
-
在第四章中,处理相机一节引入了相机对象。
-
在第四章中,处理相机一节通过使用边界相机限制相机区域进行了说明。
-
在第四章中,处理相机一节通过使用缩放相机近距离观察进行了阐述。
-
在第四章中,处理相机一节介绍了给相机应用 HUD。
-
在第五章中,场景和图层管理一节讲述了自定义管理和图层。
-
在第六章中,应用物理一节介绍了 Box2D 物理扩展的入门知识。
-
在第七章中,处理更新处理器一节解释了更新处理器是什么。
-
在第八章中,最大化性能一节讲解了创建精灵池。
LoadingRunnable.java
LoadingRunnable
类在作为Runnable
对象的同时,也会在ManagedGameScene
类中更新加载屏幕。每个ManagedGameScene
类中都存在一个LoadingRunnable
对象的ArrayList
类型,以便开发者可以控制玩家看到的加载进度。需要注意的是,虽然在 MagneTank 中更新加载屏幕不会占用太多处理器资源,但更复杂、图形复杂的加载屏幕可能会大大增加每个关卡的加载时间。
Levels.java
Levels
类保存了游戏中可以玩的所有关卡数组,以及帮助获取特定关卡的辅助方法。
BouncingPowerBar.java
BouncingPowerBar
类向玩家显示一个弹跳指示器,指示每次从车辆射击的威力大小。它将指示器的可见位置转换为一个分数值,然后应用一个立方曲线,使得在尝试实现最强大射击时更具挑战性。以下图片展示了由三张独立图片构建完成后的力量条的样子:
BouncingPowerBar
类的实现基于以下方法:
-
在第二章的处理实体中理解 AndEngine 实体
-
在第二章的处理实体中使用精灵为场景注入生命
-
在第二章的处理实体中重写 onManagedUpdate 方法
-
在第二章的处理实体中将 HUD 应用到相机上
MagneTank.java
MagneTank
类创建并控制游戏基于的车辆。它使用关节将 Box2D 刚体组合起来,创建具有物理效果的车辆,并通过BoundTouchInputs
获取玩家输入,控制车辆每个部分的运动和功能。以下图片展示了 MagneTank 构建前后的样子:
MagneTank
类基于以下配方:
-
在第二章的处理实体中理解 AndEngine 实体
-
在第二章的处理实体中使用精灵为场景注入生命
-
在第二章的处理实体中使用相对旋转
-
在第二章的处理实体中重写 onManagedUpdate 方法
-
在第四章的处理相机中使用边界相机限制摄像机区域
-
在第六章的物理应用中介绍 Box2D 物理扩展
-
在第六章的物理应用中理解不同的刚体类型
-
在第六章的物理应用中通过指定顶点创建独特的刚体
-
在第六章的物理应用中使用力、速度和扭矩
-
在第六章的物理应用中处理关节工作
-
在第七章的处理更新处理器中更新处理器是什么?
-
在第十章的深入了解 AndEngine中应用基于精灵的阴影
MagneticCrate.java
MagneticCrate
类扩展了MagneticPhysObject
类。它创建并处理了 MagneTank 车辆可发射的各种类型的箱子。每个箱子以平铺精灵的形式显示,平铺精灵的图像索引设置为箱子的类型。MagneticCrate
类利用了物理世界的ContactListener
中的 Box2D 的postSolve()
方法。以下图片展示了游戏中可用的各种大小和类型的箱子:
MagneticCrate
类基于以下食谱:
-
在第二章中了解 AndEngine 实体,使用实体
-
在第二章中使用精灵为场景注入生命,使用实体
-
重写第二章中的
onManagedUpdate
方法,使用实体 -
在第六章中介绍 Box2D 物理扩展,物理应用
-
在第六章中了解不同的物体类型,物理应用
-
在第六章中使用 preSolve 和 postSolve,物理应用
-
在第七章中更新处理程序是什么?,使用更新处理程序
MagneticOrb.java
MagneticOrb
类会在 MagneTank 当前弹射体周围创建视觉效果。它让两张旋涡图像(见下图的图像)以相反的方向旋转,以产生球形力的错觉。当装填并发射弹射体时,MagneticOrb
类会形成并逐渐消失。
MagneticOrb
类基于以下食谱:
-
在第二章中了解 AndEngine 实体,使用实体
-
在第二章中使用精灵为场景注入生命,使用实体
-
在第二章中使用相对旋转,使用实体
-
在第二章中重写
onManagedUpdate
方法,使用实体
MagneticPhysObject.java
MagneticPhysObject
类扩展了PhysObject
类,允许物体被 MagneTank 车辆抓取或释放。被抓取时,物体不仅会受到反重力作用,还会受到向 MagneTank 炮塔方向拉扯物体的力。
MagneticPhysObject
类基于以下食谱:
-
在第六章中介绍 Box2D 物理扩展,物理应用
-
在第六章中了解不同的物体类型,物理应用
-
在第六章中使用力、速度和扭矩,物理应用
-
在第六章中将反重力应用于特定物体第六章 物理应用
-
在第六章中更新处理程序是什么?,使用更新处理程序
MechRat.java
MechRat
类扩展了PhysObject
类,以利用在与其他物理启用的对象碰撞时调用的postSolve()
方法。如果力足够大,MechRat 就会被摧毁,并且之前加载的粒子效果会立即显示。MechRat 还有关节连接的轮子,这增加了摧毁它的挑战性。以下图片展示了 MechRat 的视觉组成:
这个类基于以下食谱:
-
理解 AndEngine 实体在章节 2,处理更新处理器
-
使用精灵为场景注入生命在章节 2,处理更新处理器
-
重写
onManagedUpdate
方法在章节 2,处理更新处理器 -
在章节 2 中处理粒子系统,处理更新处理器
-
Box2D 物理扩展介绍在第章节 6,物理学的应用
-
理解不同的物体类型在第章节 6,物理学的应用
-
通过指定顶点创建独特的物体在第章节 6,物理学的应用
-
处理关节在第章节 6,物理学的应用
-
使用 preSolve 和 postSolve在第章节 6,物理学的应用
-
创建可破坏的物体在第章节 6,物理学的应用
-
更新处理器是什么?在第章节 7,使用更新处理器
MetalBeamDynamic.java
这个类代表了游戏中看到的非静态、物理启用的梁。由于它的重复纹理,每根梁的长度可以设置。
MetalBeamDynamic
类基于以下食谱:
-
理解 AndEngine 实体在章节 2,使用更新处理器
-
使用精灵为场景注入生命在章节 2,处理更新处理器
-
在章节 2 中使用相对旋转,使用实体
-
重写
onManagedUpdate
方法在章节 2,使用实体 -
Box2D 物理扩展介绍在第章节 6,物理学的应用
-
理解不同的物体类型在第章节 6,物理学的应用
MetalBeamStatic.java
与上面的MetalBeamDynamic
类相似,这个类也代表一个桁架,但这个对象的BodyType
选项设置为Static
,以创建一个静止的屏障。
MetalBeamStatic
类基于以下食谱:
-
在第二章,使用实体中,了解 AndEngine 实体
-
在第二章,使用实体中,让场景通过精灵生动起来
-
在第二章,使用实体中使用相对旋转
-
在第六章,物理应用中,介绍 Box2D 物理扩展
-
在第六章,物理应用中,了解不同的身体类型
ParallaxLayer.java
由本书的合著者 Jay Schroeder 编写并发布的ParallaxLayer
类,使得创建ParallaxEntity
对象变得简单,这些对象在Camera
对象在场景中移动时能产生深度感知。可以设置视差效果的程度,ParallaxLayer
类负责正确渲染每个ParallaxEntity
对象。以下图片展示了 MagneTank 的背景层,它们附着在一个ParallaxLayer
类上:
ParallaxLayer
类基于以下食谱:
-
在第二章,使用实体中,了解 AndEngine 实体 (注意:这一行与第四行重复,根据注意事项,这里不重复翻译)
-
在第二章,使用实体中,使用 OpenGL
-
在第二章,使用实体中,重写 onManagedUpdate 方法
-
在第三章,设计你的菜单中使用视差背景创造透视感
PhysObject.java
PhysObject
类在 MagneTank 中用于委派从物理世界的ContactListener
接收到的接触。它还提供了一个destroy()
方法,使得销毁物理对象更加容易。
PhysObject
类基于以下食谱:
-
在第二章,使用实体中,了解 AndEngine 实体
-
在第六章,物理应用中,介绍 Box2D 物理扩展
-
在第六章,物理应用中,了解不同的身体类型
-
在第六章,物理应用中使用 preSolve 和 postSolve
-
更新处理程序是什么? 在第七章,使用更新处理程序
RemainingCratesBar.java
RemainingCratesBar
类为玩家提供了视觉表示,显示还有哪些箱子需要被 MagneTank 射击。每个级别剩余的箱子的大小、类型和数量从 GameLevel
类中获取,并且会从一级到另一级发生变化。当一个箱子被击中时,RemainingCratesBar
类会动画化以反映游戏状态的变化。
这个类基于以下食谱:
-
第二章中的理解 AndEngine 实体,使用实体
-
第二章中的使用精灵为场景注入生命,使用实体
-
第二章中的使用 OpenGL,使用实体
-
第二章中的覆盖 onManagedUpdate 方法,使用实体
-
第二章中的使用修改器和实体修改器,使用实体
TexturedBezierLandscape.java
TexturedBezierLandscape
类创建了两个纹理网格和一个物理体,代表关卡的地面。顾名思义,该景观由贝塞尔曲线组成,以展示上升或下降的斜坡。纹理网格由重复的纹理制成,以避免景观区域之间的可见缝隙。以下图片展示了创建景观所使用的两种纹理以及应用贝塞尔斜坡后组合网格的外观示例:
TexturedBezierLandscape
类基于以下食谱:
-
第二章中的理解 AndEngine 实体,使用实体
-
第二章中的使用 OpenGL,使用实体
-
第六章中的Box2D 物理扩展介绍,物理应用
-
第六章中的理解不同的物体类型,物理应用
-
第六章中的通过指定顶点创建独特的物体,物理应用
-
第十章中的纹理网格,深入了解 AndEngine
TexturedMesh.java
这个类与第十章中纹理网格的食谱中找到的 TexturedMesh
类相同。
WoodenBeamDynamic.java
这个类与 MetalBeam
类相似,但增加了一个健康方面,一旦其健康值达到零,就会用粒子效果替换 WoodenBeamDynamic
类。
WoodenBeamDynamic
类基于以下食谱:
-
在第二章,处理实体中理解 AndEngine 实体(注意:这里原文重复,根据注意事项,译文不应重复)
-
在第二章,处理实体中使用精灵为场景注入生命
-
在第二章,处理实体中使用相对旋转
-
在第二章,处理实体中覆盖 onManagedUpdate 方法
-
在第二章,处理实体中使用粒子系统
-
在第六章,物理应用中Box2D 物理扩展介绍
-
在第六章,物理应用中理解不同的身体类型
-
在第六章,物理应用中使用 preSolve 和 postSolve
-
在第七章,使用更新处理器中更新处理器是什么?
输入类
这些类中的每一个都处理游戏中使用的特定输入方法:
BoundTouchInput.java
BoundTouchInput
类便于输入的委托,然后这些输入绑定到 BoundTouchInput
类。这可以在游戏中轻松看到,例如移动 MagneTank 以瞄准炮塔时。当触摸进入另一个可触摸区域时,它仍保持与原始区域的绑定。
GrowButton.java
GrowButton
类仅显示一个图像,当玩家触摸它时,它会增长到特定的比例,并在触摸抬起或丢失时恢复到原始比例。
本类基于以下食谱:
-
在第二章,处理实体中理解 AndEngine 实体
-
在第二章,处理实体中使用精灵为场景注入生命
-
在第二章,处理实体中覆盖 onManagedUpdate 方法
-
在第二章,处理实体中使用修改器和实体修改器
GrowToggleButton.java
本类基于 GrowButton
类,并增加了根据条件状态显示一个或两个 TiledTextureRegion
索引的功能。
GrowToggleButton
类基于以下食谱:
-
在第二章,处理实体中理解 AndEngine 实体
-
在第二章,处理实体中使用精灵为场景注入生命
-
在第二章,处理实体中覆盖 onManagedUpdate 方法
-
在第二章,处理实体中使用修改器和实体修改器
GrowToggleTextButton.java
基于GrowToggleButton
类,这个类使用Text
对象而不是TiledTextureRegion
对象来显示条件的状态。
GrowToggleTextButton
类基于以下配方:
-
在第二章,处理实体中理解 AndEngine 实体
-
在第二章,处理实体中使用精灵让场景生动起来
-
在第二章,处理实体中将文本应用到层上
-
在第二章,处理实体中覆盖 onManagedUpdate 方法
-
在第二章,处理实体中使用修饰符和实体修饰符
层类
这些类表示游戏内存在的层:
LevelPauseLayer.java
LevelPauseLayer
类表示当关卡暂停时显示给玩家的层。它显示当前的关卡号码、分数和最高分,以及返回游戏、返回关卡选择屏幕、重新开始关卡或跳转到下一关卡的按钮。
这个类基于以下配方:
-
在第二章,处理实体中理解 AndEngine 实体
-
在第二章,处理实体中使用精灵让场景生动起来
-
在第二章,处理实体中将文本应用到层上
-
在第五章,场景和层管理中自定义管理场景和层
-
在第七章,处理更新处理器中更新处理器是什么?
LevelWonLayer.java
LevelWonLayer
类表示当玩家成功完成一个关卡时显示给玩家的层。它显示当前的关卡号码、分数和最高分,以及玩家获得的星级评价。还包括返回关卡选择屏幕、重玩关卡或进入下一关卡的按钮。以下图片展示了LevelWonLayer
类的纹理以及它们在游戏中组合起来的样子:
LevelWonLayer
类基于以下配方:
-
在第二章,处理实体中理解 AndEngine 实体
-
在第二章,处理实体中使用精灵让场景生动起来
-
在第二章,处理实体中将文本应用到层上
-
在第二章,处理实体中使用修饰符和实体修饰符
-
在第五章中自定义管理场景和图层,场景和图层管理
-
第七章中的更新处理器是什么?,使用更新处理器
ManagedLayer.java
这个类与在第五章中创建场景管理器的食谱中找到的ManagedLayer
类是相同的,场景和图层管理。
OptionsLayer.java
这个图层可以从MainMenu
场景访问,允许玩家启用或禁用音乐和声音,以及选择图形质量或重置他们已完成的关卡完成数据。
OptionsLayer
类基于以下食谱:
-
在第二章中了解 AndEngine 实体,使用实体
-
在第二章中使用精灵使场景生动,使用实体
-
在第二章中将文本应用于图层,使用实体
-
在第五章中自定义管理场景和图层,场景和图层管理
-
第七章中的更新处理器是什么?,使用更新处理器
管理类
这些类各自管理游戏的一个特定方面:
GameManager.java
GameManager
类简单地为检查两个条件以确定一个关卡是否完成或失败提供便利。使用该信息,游戏管理器随后调用在GameLevel
类中设置的正确方法。
这个类基于以下食谱:
-
在第一章中创建游戏管理器,AndEngine 游戏结构
-
第七章中的更新处理器是什么?,使用更新处理器
ResourceManager.java
ResourceManager
类与在第一章中找到的类非常相似,AndEngine 游戏结构,但它增加了如果需要可以使用一组低质量纹理的能力。它还包括用于确定精确字体纹理大小的方法,以防止浪费宝贵的纹理内存。
这个类基于以下食谱:
-
在第一章中应用纹理选项,AndEngine 游戏结构
-
在第一章中使用 AndEngine 字体资源,AndEngine 游戏结构
-
在第一章中创建资源管理器,AndEngine 游戏结构
-
在第二章中使用 OpenGL,使用实体*
-
在第五章的场景和图层管理部分,设置场景资源的资源管理器《为场景资源设置资源管理器》
SceneManager.java
这个类与第五章中的创建场景管理器食谱中的SceneManager
类完全相同《场景和图层管理》
SFXManager.java
这个简单的类处理音乐和声音的播放以及它们的静音状态。
SFXManager
类基于以下食谱:
- 在第一章的AndEngine 游戏结构部分,介绍声音和音乐《介绍声音和音乐》
菜单类
这些类仅用于游戏中的菜单。
LevelSelector.java
这个类与第三章中的菜单设计中的关卡选择器类似,但使用一系列LevelSelectorButton
对象代替了精灵《设计你的菜单》
这个类基于以下食谱:
-
在第二章的使用实体工作部分,了解
AndEngine
实体《了解 AndEngine 实体》 -
在第二章的使用实体工作部分,《使用精灵使场景生动》
-
在第三章的设计你的菜单部分,创建我们的关卡选择系统《创建我们的关卡选择系统》
LevelSelectorButton.java
LevelSelectorButton
类通过视觉向玩家展示一个关卡的当前状态,是锁定还是解锁,如果关卡已解锁,还会显示获得的星星数量。
这个类基于以下食谱:
-
在第二章的使用实体工作部分,了解
AndEngine
实体《了解 AndEngine 实体》 -
在第二章的使用实体工作部分,《使用精灵使场景生动》
-
在第二章的使用实体工作部分,将文本应用到图层《将文本应用到图层》
-
在第二章的使用实体工作部分,覆盖
onManagedUpdate
方法《覆盖 onManagedUpdate 方法》 -
在第二章的使用实体工作部分,使用修改器和实体修改器《使用修改器和实体修改器》
MainMenu.java
MainMenu
类包含两个Entity
对象,一个代表标题屏幕,另一个代表关卡选择屏幕。两个屏幕之间的切换是通过实体修改器实现的。在首次显示主菜单时,会显示加载屏幕,同时加载游戏的资源。
MainMenu
类基于以下食谱:
-
在第二章的使用实体工作部分,了解
AndEngine
实体《了解 AndEngine 实体》 -
在第二章的使用实体工作部分,介绍如何通过精灵使场景生动《使用精灵使场景生动》
-
在第二章的使用实体工作部分,覆盖
onManagedUpdate
方法《覆盖 onManagedUpdate 方法》 -
在第二章,处理实体中使用修改器和实体修改器。
-
在第五章,场景和图层管理中自定义管理场景和图层。
ManagedMenuScene.java
这个类与第五章,场景和图层管理中创建场景管理器食谱中呈现的ManagedMenuScene
类相同。
ManagedSplashScreen.java
这个类基于第五章场景和图层管理中自定义管理场景和图层食谱中找到的ManagedMenuScene
类。它添加了代码,在隐藏启动画面后卸载Entity
对象。
SplashScreens.java
SplashScreen
类使用实体修改器和与分辨率无关的定位来显示游戏的启动画面。每个标志都是可点击的,并启动与标志相关的意图。
这个类基于以下食谱:
-
在第二章,处理实体中使用精灵让场景生动。
-
在第二章,处理实体中将文本应用到图层。
-
在第二章,处理实体中使用修改器和实体修改器。
-
在第五章,场景和图层管理中自定义管理场景和图层。
-
在第七章,处理更新处理器中更新处理器是什么?。
活动和引擎类
这些类是游戏的核心。
MagneTankActivity.java
这个活动类基于标准的 AndEngine BaseGameActivity
类,通过在onCreateEngineOptions()
方法中添加广告和一些高级分辨率缩放以及共享首选项方法来保存和恢复选项和分数。
这个类基于以下食谱:
-
在第一章,AndEngine 游戏结构中了解生命周期。
-
在第一章,AndEngine 游戏结构中选择我们的引擎类型。
-
在第一章,AndEngine 游戏结构中保存和加载游戏数据。
-
在第五章,场景和图层管理中设置活动以使用场景管理器。
MagneTankSmoothCamera.java
这个类扩展了SmoothCamera
对象,但包括在指定时间内平移到敌方基地以及跟踪MagneTank
对象的能力。
这个类基于以下食谱:
-
在第四章,使用相机中介绍相机对象。
-
在第四章的使用平滑摄像头创建平滑移动部分,使用摄像头
-
在第七章的什么是更新处理器?部分,使用更新处理器
ManagedScene.java
这个类与第五章中创建场景管理器一节中介绍的是同一个ManagedScene
类,场景和图层管理
SwitchableFixedStepEngine.java
当调用了EnableFixedStep()
方法时,这个Engine
对象的行为与FixedStepEngine
对象完全一样。
这个类基于以下食谱:
-
在第一章的选择我们的引擎类型部分,AndEngine 游戏结构
-
在第七章的什么是更新处理器?部分,使用更新处理器