将 2D libgdx 游戏移植到 MonoGame





5.00/5 (18投票s)
将您的 libgdx 游戏移植到 Windows Phone 和 Windows 8 平台运行。
引言
Libgdx 是一个非常流行的便携式游戏引擎。开发人员使用 Java 代码,可以轻松地将游戏发布到 Java 桌面、HTML(使用 WebGL)、Android 和 iOS(使用 RoboVM)。但是,不支持 Windows Phone 和 Windows 导出。主要原因是 libgdx 是一个 OpenGL 框架。Libgdx 开发人员一直在努力创建可能是最高效的 OpenGL 引擎。这是 libgdx 最强大的优点之一。然而,Windows Phone 和 Windows 平台基于 DirectX。尽管有一些库同时支持 OpenGL 和 DirectX (cocos2d-x),但我认为 libgdx 走这条路线的可能性很小。
这意味着数以千计使用 libgdx 开发的游戏将很难在 Windows Phone 和 Windows 平台上找到出路。这对游戏玩家来说是一种遗憾,他们将无法玩到一些真正出色的游戏;对开发人员来说也是一种遗憾,他们将失去新的机会。
在本文中,我将尝试说服 libgdx 开发人员,将他们的游戏移植到 MonoGame 远非困难和耗时。MonoGame 是另一个跨平台引擎,它依赖于 C# 语言,可以将游戏发布到 Android、iOS,当然还有 Windows Phone 和 Windows 平台。我们的目标当然是针对 Windows Phone 和 Windows,因为其他平台已经由 libgdx 覆盖。为了展示过渡的简易性,我将提供三个 libgdx 开源游戏的 MonoGame 版本。
MonoGame 框架
在本文中,我不会提供安装 MonoGame 和启动新项目的说明。这些信息在其他地方已经可用。Code Project 中有一篇非常翔实的文章在此发布。MonoGame 文档列出了教程。列出了免费和付费资源。在免费资源中,我发现 RB Whitaker 的 Wiki 教程非常有帮助。我还想推荐《Windows 8 and Windows Phone 8 Game Development》这本书。(我与出版商或作者没有任何关系。)
libgdx 开发人员的 MonoGame
无论如何,我建议您立即开始编写 C# 代码,并在遇到特定问题时搜索互联网。您会惊讶于可以重用多少 Java 知识。在某个时候,您可能想读一本书来建立您的理论知识,但这对于移植游戏来说不是必需的。为了帮助您,我创建了一个简短的 Java-C# 语法比较表。这个表当然不详尽,但仅凭它的帮助,您将能够将 90% 或更多的 Java 代码移植到 C#。这些实际上是我在将 libgdx 游戏移植到 MonoGame 时所做的笔记。
Java - C# 比较
包 - 命名空间 | |
Java | C# |
package com.rengelbert.froggergdx;
import com.rengelbert.froggergdx.data.GameData;
import com.rengelbert.froggergdx.screens.Screen;
public class Game implements ApplicationListener {
}
|
using System;
using FruitCatcher.Text;
using FruitCatcher.Models;
namespace FruitCatcher
{
public class FruitCatcherGame : ScreensGame
{
}
}
|
Java 包的等价物是命名空间。语法不同,但 IDE 是您的朋友。当您创建类时,Visual Studio 将添加命名空间声明和一些常见的 using 语句。在 Eclipse 中,您可以选择“源”,“组织导入”以自动添加导入语句并删除不必要的语句。Visual Studio 不提供此功能(有插件提供)。最接近您能做的是右键单击 C# 代码中缺失的类并选择“解决”。 |
构建路径 - 引用 | |
Eclipse | Visual Studio |
![]() |
![]() |
在 Java 中,您将第三方库添加到构建路径。在 C# 中,您将程序集添加为引用。您可以添加对系统库、文件系统中 dll 文件或库项目输出的引用。但是,在 Visual Studio 中,有更好的方法来添加第三方库。您可以使用 NuGet。通过图形界面或命令行,NuGet 将允许您轻松地将第三方库集成到您的代码中。它不仅会添加所有必要的引用,还会为您进行所有需要的代码和配置更改。 |
扩展类。接口 | |
Java | C# |
public class Game implements ApplicationListener {}
public class MyGame extends BaseGame {}
|
public class Game : ApplicationListener
public class MyGame : BaseGame
|
C# 使用相同的语法来扩展类和实现接口。 |
Types | |
Java | C# |
boolean
Integer.parseInt("12");
List<Integer> list = new ArrayList<integer>();
</integer>
|
bool
int.Parse("12");
List<int> list = new List<int>();
</int>
|
在 C# 中,您需要使用 `bool` 类型而不是 `boolean`。Java 中的其他原始类型在 C# 中具有相同的名称。然而,在 C# 中,没有原始类型的概念。所有类型都是对象。这使得您的生活更轻松,尤其是在集合中。 |
final vs const 和 readonly | |
Java | C# |
public static final int GAME_STATE_PLAY = 0;
private final int ANIMATION_DURATION = 100;
|
public const int GAME_STATE_PLAY = 0;
private readonly int ANIMATION_DURATION = 100;
|
在 C# 中,`final` 关键字映射到 `const` 或 `readonly`。这不是一对一的关系。您可以在这里阅读完整的讨论。对于大多数情况,上面两个例子就足够了。 |
super vs base | |
Java | C# |
public class MyExceptionClass extends Exception
{
public MyExceptionClass(string msg, string extrainfo)
{
super(msg);
}
}
|
public class MyExceptionClass : Exception
{
public MyExceptionClass(string msg, string extrainfo)
: base(msg)
{
}
}
|
您使用 `base` 而不是 `super` 来访问父类方法。构造函数语法不同。 |
getter 和 setter vs 属性 | |
Java | C# |
class TimePeriod
{
private double seconds;
public double getHours() {
return seconds / 3600;
}
public void setHours(double hours){
seconds = hours * 3600;
}
}
|
class TimePeriod
{
private double seconds;
public double Hours
{
get { return seconds / 3600; }
set { seconds = value * 3600; }
}
}
|
Java 使用 getter 和 setter 在类之间传递值。C# 使用属性。您无需修改代码来使用属性,因为 getter 和 setter 模式在 C# 中也能很好地工作。但是,您绝对需要理解属性概念,因为它被系统库和 MonoGame 框架大量使用。 |
注解 vs 属性 | |
Java | C# |
import com.google.gson.annotations.SerializedName;
public class PurchaseOrder
{
@SerializedName("purchaseId")
private int poId_value;
}
|
[DataContract]
public class PurchaseOrder
{
private int poId_value;
[DataMember]
public int PurchaseOrderId
{
get { return poId_value; }
set { poId_value = value; }
}
}
|
Java 注解的等价物是 C# 属性。语法不同,但理念相同。您通过注解类或字段来添加所需的行为。另一个区别是 `@Override` 注解,它在 C# 中被 `override` 和 `virtual` 关键字取代。 |
编码约定 | |
Java | C# |
public class A {
public void methodName() {
}
}
line.trim().length()
list.size()
list.add(item)
|
public class A
{
public void MethodName()
{
}
}
line.Trim().Length
list.Count // Property
list.add(item)
|
Java 和 C# 使用不同的编码风格。C# 鼓励每个大括号都另起一行。此外,大小写风格略有不同。也许最重要的区别是 C# 中的方法名使用帕斯卡命名法(首字母大写)。在移植代码时,您无需遵循这些约定。但是,这些信息将帮助您理解系统和框架库。 |
集合 | |
Java | C# |
List<String> l = new ArrayList<string>();
Map<string,> m = new HashMap<string,>();
l.size()
l.add(item), l.remove(item)
l.get(i)
</string,>
|
List<String> l = new List<string>();
Dictionary<string,> m = new Dictionary<string,>();
l.Count // Property
l.Add(item), l.Remove(item)
l[i] // Operation overloading
</string,>
|
在 C# 中,我们通常不使用接口来定义集合。通用接口(IEnumerable、ICollection)确实存在。List 映射到 C# 中的 List,Map 映射到 Dictionary。大多数方法在 C# 中具有相同的名称,唯一的区别是首字母大写。这将使您的移植更容易。C# 支持运算符重载,因此您可以使用数组语法 `[i]` 来检索项目,而不是 `get(i)`。 |
for-each 循环 | |
Java | C# |
for(String s: list) {
}
|
foreach(String s in list)
{
}
|
在 C# 中,您使用 `foreach` 语句来实现 for-each 循环。 |
枚举类型 | |
Java | C# |
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
private double mass() { return mass; }
private double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;
double surfaceGravity() {
return G * mass / (radius * radius);
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
public static void main(String[] args) {
if (args.length != 1) {
System.err.println(
"Usage: java Planet <earth_weight>");
System.exit(-1);
}
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf(
"Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
}
|
public enum Planet {
MERCURY = 0,
VENUS,
EARTH,
MARS,
JUPITER,
SATURN,
URANUS,
NEPTUNE
}
public static class PlanetExtensions {
private static double [] masses = {
3.303e+23, 4.869e+24, 5.976e+24, 6.421e+23,
1.9e+27, 5.688e+26, 8.686e+25, 1.024e+26};
private static double [] radii = {
2.4397e6, 6.0518e6, 6.37814e6, 3.3972e6,
7.1492e7, 6.0268e7, 2.5559e7, 2.4746e7};
public static double Mass(this Planet planet)
{
return masses[(int) planet];
}
public static double Radius(this Planet planet)
{
return radii[(int) planet];
}
}
Console.WriteLine(Planet.MARS.Mass().ToString());
|
`enum` 类型可能是 Java 提供比 C# 更高级功能的唯一语言特性。您不能在枚举中拥有构造函数或方法。此外,只允许整数类型。您可以使用扩展方法来添加缺失的功能。 |
正如我之前所说,上面的列表远非完整。其他更改包括 C# 没有受检查异常,提供结构体和`可空`类型,支持闭包和许多函数式编程功能,例如LINQ。您不需要所有这些来入门,可以在后期阶段学习所有内容。
游戏循环
libgdx 引擎通过 ApplicationListener 接口实现游戏循环。在此基础上,开发人员通常利用 Game 和 Screens 模式。在 MonoGame 中,游戏循环通过重写 Game 类来实现。下面将比较这两种实现。
libgdx | MonoGame |
public class Drop
implements ApplicationListener {
@Override
public void create() {
// load the images and sound effects
// start the playback of the
// background music
// create resources
}
@Override
public void render() {
// update
// draw
// process user input
}
@Override
public void dispose() {
// dispose of all the native resources
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
}
|
public class FroggerGame : Game
public FroggerGame()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
/// <summary>
/// Allows the game to perform any initialization it
/// needs to before starting to run.
/// This is where it can query for any required
/// services and load any non-graphic
/// related content.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and
/// is the place to load all of your content.
/// </summary>
protected override void LoadContent()
{
}
/// <summary>
/// UnloadContent will be called once per game and
/// is the place to unload all content.
/// </summary>
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Allows the game to run logic such as updating
/// the world, checking for collisions, gathering
/// input, and playing audio.
/// </summary>
/// <param name="gameTime" />Provides a snapshot of
/// timing values.
protected override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime" />Provides a snapshot of
/// timing values.
protected override void Draw(GameTime gameTime)
{
}
}
|
如您所见,有很多相似之处。有些方法在游戏开始之前调用代码 (`Initialize`)。有些方法在游戏启动时运行一次并加载所有必要的内容文件 (`create` - `LoadContent`)。还有些方法每帧运行一次 (`render` - `Update`, `Draw`)。MonoGame 将 `render` 方法拆分为 `Update` 和 `Draw`。前者用于更新游戏状态,后者用于绘制游戏状态。大多数经验丰富的开发人员在 libgdx 中也这样做。然而,MonoGame 更进一步,能够以不同的间隔调用 `Update` 和 `Draw`。MonoGame 将每秒调用 `Update` 60 次(这是默认帧速率 - FPS),并将尝试尽可能多地调用 `Draw`。这确保了游戏状态的一致性,即使帧速率急剧下降。在 libgdx 中,您需要计算帧速率,如果帧速率太低,则在渲染期间跳过绘图部分。
另一个明显的区别是 MonoGame 不支持生命周期事件(`pause`、`resume`)。MonoGame 源于 XNA,主要面向桌面和主机世界。这些生命周期事件在这些平台上没有意义,也没有引入 MonoGame。然而,它们是移动游戏开发的一个重要方面。稍后我将向您展示如何为 Windows Phone 8 编写原生代码,以便将它们添加到您的游戏中。
内容加载
下表描绘了内容在这两个框架中的加载方式。
libgdx | MonoGame |
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
BitmapFont font;
@Override
public void create() {
dropImage =
new Texture(Gdx.files.internal("droplet.png"));
bucketImage =
new Texture(Gdx.files.internal("bucket.png"));
dropSound =
Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic =
Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
font = new BitmapFont(
Gdx.files.internal("fonts/poetsen.fnt"),
Gdx.files.internal("fonts/poetsen.png"),
false);
}
|
private Texture2D background;
private SpriteFont font;
private Song music;
private SoundEffect jumpSound;
/// <summary>
/// LoadContent will be called once per game and is the
/// place to load all of your content.
/// </summary>
protected override void LoadContent()
{
background =
loadTexture(content, "data/background");
font =
content.Load("data/Miramonte");
jumpSound =
content.Load<SoundEffect>("data/jump");
music =
content.Load<Song>("data/music");
}
|
代码类似,但与 libgdx 相反,MonoGame 不能直接加载资产。图像、声音和字体需要先处理并转换为特定格式。这目前通过 XNA 内容项目实现。有关更多详细信息,我建议您阅读本教程。这个过程比它应该的更复杂,它要求您安装 Visual Express 2012 for Windows Phone(或者如果您有专业版,则安装 Windows Phone SDK),即使您不需要为 Windows Phone 构建。希望 MonoGame 会发展到完全支持内容管线。(在我写这篇文章时,MonoGame 3.2 发布了。其中一个新功能是:*大量内容管线改进使其接近完成。*)
2D 绘图
libgdx 和 MonoGame 中 2D 绘图的主要神器都是 SpriteBatch(libgdx,MonoGame)。如您所见,代码非常相似。您调用 `begin` 方法,然后为每个要渲染到屏幕上的纹理调用 `draw`。在两个版本中,您都可以有多个具有不同参数的 begin-end 循环。在 libgdx 中,您使用 `batch.setXXX` 方法进行参数化。在 MonoGame 中,您将参数传递给 `batch.Begin()`。(参数不是一一对应的。)
libgdx | MonoGame |
SpriteBatch batch;
OrthographicCamera camera;
@Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();
}
|
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime" />Provides a snapshot
/// of timing values.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
batch.Begin();
Vector position = new Vector2(0, 0);
batch.Draw(bgTexture, position, Color.White);
batch.End();
base.Draw(gameTime);
}
|
这两个框架中的 `draw` 方法都有不同的形式。根据您想要实现的目标,选择其中一种。在这里介绍所有不同的版本会很困难。然而,由于这些方法是 2D 绘图的核心,我仍然想比较两个最完整的版本。它们的接口如下所示。
libgdx | MonoGame |
/** Draws a rectangle with the bottom left corner at x,y having the given width and height in pixels. The rectangle is offset by originX, originY relative to the origin. Scale specifies the scaling factor by which the rectangle should be scaled around originX, originY. Rotation specifies the angle of counter clockwise rotation of the rectangle around originX, originY. The portion of the {@link Texture} given by srcX, srcY and srcWidth, srcHeight is used. These coordinates and sizes are given in texels. FlipX and flipY specify whether the texture portion should be flipped horizontally or vertically.
* @param x the x-coordinate in screen space
* @param y the y-coordinate in screen space
* @param originX the x-coordinate of the scaling and rotation origin relative to the screen space coordinates
* @param originY the y-coordinate of the scaling and rotation origin relative to the screen space coordinates
* @param width the width in pixels
* @param height the height in pixels
* @param scaleX the scale of the rectangle around originX/originY in x
* @param scaleY the scale of the rectangle around originX/originY in y
* @param rotation the angle of counter clockwise rotation of the rectangle around originX/originY
* @param srcX the x-coordinate in texel space
* @param srcY the y-coordinate in texel space
* @param srcWidth the source with in texels
* @param srcHeight the source height in texels
* @param flipX whether to flip the sprite horizontally
* @param flipY whether to flip the sprite vertically */
public void draw (Texture texture,
float x,
float y,
float originX,
float originY,
float width,
float height,
float scaleX,
float scaleY,
float rotation,
int srcX,
int srcY,
int srcWidth,
int srcHeight,
boolean flipX,
boolean flipY)
// A texel is a pixel from the texture.
|
// Overload for calling Draw() with named parameters
/// <summary>
/// This is a MonoGame Extension method for calling Draw() using named parameters. It is not available in the standard XNA Framework.
/// </summary>
/// <param name='texture'>
/// The Texture2D to draw. Required.
/// </param>
/// <param name='position'>
/// The position to draw at. If left empty, the method will draw at drawRectangle instead.
/// </param>
/// <param name='drawRectangle'>
/// The rectangle to draw at. If left empty, the method will draw at position instead.
/// </param>
/// <param name='sourceRectangle'>
/// The source rectangle of the texture. Default is null
/// </param>
/// <param name='origin'>
/// Origin of the texture. Default is Vector2.Zero
/// </param>
/// <param name='rotation'>
/// Rotation of the texture. Default is 0f
/// </param>
/// <param name='scale'>
/// The scale of the texture as a Vector2. Default is Vector2.One
/// </param>
/// <param name='color'>
/// Color of the texture. Default is Color.White
/// </param>
/// <param name='effect'>
/// SpriteEffect to draw with. Default is SpriteEffects.None
/// </param>
/// <param name='depth'>
/// Draw depth. Default is 0f.
/// </param>
public void Draw (Texture2D texture,
Vector2? position = null,
Rectangle? drawRectangle = null,
Rectangle? sourceRectangle = null,
Vector2? origin = null,
float rotation = 0f,
Vector2? scale = null,
Color? color = null,
SpriteEffects effect = SpriteEffects.None,
float depth = 0f)
|
这两个方法名称相似,您应该能够将一个映射到另一个。MonoGame 倾向于将参数打包在 `Vector2` 和 `Rectangle` 对象中,而在 libgdx 中,我们有单独的参数。在 libgdx 中,我们通过 `flipX` 和 `flipY` 参数进行翻转。在 MonoGame 中,使用 SpriteEffects。在 libgdx 中,您通过在调用 `draw` 之前调用 SpriteBatch.setColor 方法来实现颜色着色效果。在 MonoGame 中,颜色是 `Draw` 方法本身的参数。
注意:语法 `Rectangle?` 表示一个可空类型。`Rectangle` 是一个结构体,因此是一个值类型。这意味着它类似于 `int`,它应该始终有一个值 - 不允许为 `null`。相反,您可以将 `null` 值设置为 `Rectangle?` 和 `int?`。
虽然 2D 绘图示例看起来相似,但这两个框架在处理 2D 绘图的方式上实际存在重要差异。其中具有最实际影响的是:
- MonoGame 不支持 TextureRegion。在 libgdx 中,您可以直接或通过 TextureAtlas 创建 TextureRegion (大图像的一部分)。此外,还提供了专门的工具,可以帮助您将许多图像打包成一个,并生成一个可以直接由 libgdx 使用的位置和尺寸文档。`draw` 方法可以接受 Texture 和 TextureRegion 作为输入。在 MonoGame 中,您需要手动完成上述所有操作。您需要将 `sourceRectangle` 参数传递给 `Draw` 方法,该参数定义要绘制图像的哪一部分。将 `TextureRegion` 和 `TextureAtlas` 移植到 MonoGame 并不困难。我将提供代码,允许您使用由 libgdx 工具生成的相同打包图像。
- MonoGame 和 libgdx 使用不同的坐标系。在 libgdx 中,y 轴从左下角开始向上。在 MonoGame 中,它从左上角开始向下。这非常不幸,因为您需要从头开始进行所有位置计算。一个简单的解决方案是围绕 `Draw` 方法创建一个包装器,该包装器将考虑坐标系差异并自动进行转换。

- 最后,最重要的区别是 libgdx 中的 `SpriteBatch` 可以将 Camera 作为输入参数。MonoGame 不将相机与 2D 绘图相关联。这是一个重要的遗漏,因为在 libgdx 中,相机在 2D 游戏中大量使用。它最常见的用途是使游戏世界适应屏幕尺寸。您可以使用固定的视口尺寸完成所有工作,相机将进行缩放以适应设备的屏幕。输入位置也可以轻松转换为固定尺寸。在 MonoGame 中,您使用其他技术来实现相同的功能。我前面提到的一个简单的技术,涉及 `DrawingSurface`,在这篇出色的 Code Project MonoGame 文章中进行了描述。另一个解决方案在此描述。相机还用于实现各种事件,例如视差滚动。在Super Jumper游戏中,它用于创建角色向上移动的错觉。弥补相机缺失是我在移植代码时面临的最大挑战。我使用的解决方案是将所有相机逻辑添加到 `Draw` 方法的包装器中。现在包装器还需要知道屏幕尺寸,以便正确缩放和放置图像。对于 Super Jumper 相机效果,我只需要添加一个相机位置。图像是相对于相机位置绘制的。
LibgdxHelper 库
当我萌生将 libgdx 游戏移植到 MonoGame 的想法时,我的雄心是创建构造,以便代码差异尽可能少。这个想法是,人们能够快速移植游戏,而无需编辑图像和其他资产或重新计算屏幕上的位置。为了实现这个想法,我创建了一个名为 LibgdxHelper 的库项目,它提供类似于 libgdx 的类和方法。该库提供了什么可以在下表中看到,该表比较了相同代码的 libgdx 和 MonoGame 版本。代码看起来惊人地相似。
libgdx | MonoGame |
// Loading images
public static Texture loadTexture (String file) {
return new Texture(Gdx.files.internal(file));
}
public static void load () {
background = loadTexture("data/background.png");
backgroundRegion = new TextureRegion(background, 0, 0, 320, 480);
items = loadTexture("data/items.png");
mainMenu = new TextureRegion(items, 0, 224, 300, 110);
pauseMenu = new TextureRegion(items, 224, 128, 192, 96);
ready = new TextureRegion(items, 320, 224, 192, 32);
// Drawing images
private void renderPlatforms () {
int len = world.platforms.size();
for (int i = 0; i < len; i++) {
Platform platform = world.platforms.get(i);
TextureRegion keyFrame = Assets.platform;
if (platform.state == Platform.PLATFORM_STATE_PULVERIZING) {
keyFrame = Assets.brakingPlatform.getKeyFrame(
platform.stateTime, Animation.ANIMATION_NONLOOPING);
}
batch.draw(keyFrame,
platform.position.x - 1, platform.position.y - 0.25f,
2, 0.5f);
}
}
|
// Loading images
public static Texture2D loadTexture(ContentManager content, String file) {
return content.Load<Texture2D>(file);
}
public static void load(ContentManager content)
{
background = loadTexture(content, "data/background");
backgroundRegion = new TextureRegion(background, 0, 0, 320, 480);
items = loadTexture(content, "data/items");
mainMenu = new TextureRegion(items, 0, 224, 300, 110);
pauseMenu = new TextureRegion(items, 224, 128, 192, 96);
ready = new TextureRegion(items, 320, 224, 192, 32);
// Drawing images
private void renderPlatforms()
{
int len = world.platforms.Count;
for (int i = 0; i < len; i++)
{
Platform platform = world.platforms[i];
TextureRegion keyFrame = Assets.platform;
if (platform.state == Platform.PLATFORM_STATE_PULVERIZING)
{
keyFrame = Assets.brakingPlatform.getKeyFrame(
platform.stateTime, Animation.ANIMATION_NONLOOPING);
}
spriteWorld.DrawRegion(keyFrame,
platform.position.X - 1, platform.position.Y - 0.25f,
2, 0.5f);
}
}
|
注意:虽然 LibgdxHelper 库在两个项目中运行良好,但它绝不处于可供任何项目重复使用的状态。它仍然有一些遗漏,使其不适用于一般情况。但是,我相信它是一个非常好的起点,您将能够轻松添加缺失的部分。
LibGdxHelper 提供的第一件事是 `TextureRegion` 和 `TextureAtlas` 的实现。这些是直接从 libgdx 的源代码移植过来的。然而,只迁移了绝对必要的部分。提供的 `TextureRegion` 类只是区域信息(打包图像内的位置和大小)的占位符以及对实际纹理的引用。这仍然足以让我们创建一个 `Draw` 方法,该方法接受一个参数并正确绘制图像的一部分。`TextureAtlas` 类可以读取由 libgdx 图像打包工具创建的相同信息文件。它维护一个 TextureRegions 列表,可以像 libgdx 中一样访问。这使我们能够重用相同的打包图像和相同的信息文件。这大大节省了时间。事实上,我相信通过一些性能优化,这个解决方案也可以应用于新的 MonoGame 项目。这些类在 MonoGame 中的使用如下所示:
// Create a TextureRegion out of Texture
items = content.Load<Texture2D>("data/items");
mainMenu = new TextureRegion(items, 0, 224, 300, 110);
pauseMenu = new TextureRegion(items, 224, 128, 192, 96);
ready = new TextureRegion(items, 320, 224, 192, 32);
// Use a TextureAtlas to create TextureRegions
public class ImageCache
{
public bool IsLoaded { get; private set; }
public static Texture2D sheet;
public static TextureAtlasData atlas;
public ImageCache()
{
IsLoaded = false;
}
public async Task Load(ContentManager Content)
{
sheet = Content.Load<Texture2D>("frogger");
atlas = new TextureAtlasData();
// frogger.txt is the file created by libgdx
// It must be added to the project as Content
await atlas.Load("Data\\frogger.txt", sheet);
IsLoaded = true;
}
// The result of this should be cached and not called every time
public static TextureRegion getTexture(String name)
{
return atlas.FindRegion(name);
}
// The result of this should be cached and not called every time
public static TextureRegion getFrame(String name, int index)
{
return atlas.FindRegion(name, index);
}
}
注意:我没有对 LibgdxHelper 进行性能方面的考虑。我的目标是开发速度。对于我将要展示的简单游戏来说,性能绰绰有余。对于更复杂的游戏可能并非如此。
提供的第二个神器试图解决前面描述的另外两个主要问题(不同的坐标系,缺少相机)。它被称为 `SpriteBatch`,它基本上在 `Draw` 方法周围创建了一个智能包装器。为了工作,`SpriteBatch` 需要知道设备屏幕的尺寸。这是通过在构造函数中传递 `Game` 的 `GraphicsDevice` 对象来实现的。它还需要知道所有计算都已完成的基视口的尺寸。这是通过调用 `SpriteBatch.Resize(float width, float height)` 方法来实现的。最后,它可以接受一个 `OrthographicCamera` 对象。这模拟了 libgdx 中的正交相机。该实现只是封装了相机位置 (`Vector2`) 和相机视锥 (`Vector3` 尺寸)。这些信息允许 `SpriteBatch` 提供 `Draw` 方法,这些方法可以在 `Texture2D` 和 `TextureRegion` 对象上操作,其签名和功能与 libgdx `draw` 方法等效。这就是上面两个代码示例看起来如此相似的原因。`SpriteBatch` 还提供了一个方法,可以将屏幕上的触摸点转换为游戏的视口。该方法名为 `GetWorldPos(Vector2 screenPos)`,它等效于 libgdx 的 `camera.unproject(Vector3 touchPos)`。
注意:我只在 Super Jumper 游戏的上下文中测试了正交相机操作。不保证它在一般情况下会起作用。
常见任务
在屏幕上绘图并不是创建游戏所需的唯一事情。我将在这里列出一些常见的游戏开发任务以及它们在 MonoGame 中的实现方式。我不会详细介绍每个任务。相反,我将为您提供足够的信息以开始使用。
检测用户输入
MonoGame 为检测用户输入提供了强大的支持。它支持多点触控事件和手势识别。以下代码片段展示了如何检测屏幕上的触摸。
TouchCollection touches = TouchPanel.GetState();
if (touches.Count == 1 && touches[0].State == TouchLocationState.Released)
{
touchPoint = game.SpriteWorld.GetWorldPos(touches[0].Position);
if (backBounds.Contains(touchPoint.X, touchPoint.Y))
{
Assets.playSound(Assets.clickSound);
game.Screen = new MainMenuScreen(this.game);
}
}
使用位图字体
位图字体通过内容项目在 MonoGame 中加载。您不需要特殊的工具。通过内容项目,您可以从系统中安装的每个字体创建 XNB 文件(使用字体时应注意许可证)。创建 XNB 文件后,您可以轻松地将其加载到代码中。
SpriteFont font = Content.Load<SpriteFont>("Miramonte");
然后在 Draw 方法中
spriteBatch.DrawString(Assets.font, "text", new Vector2(x, y), Color.Orange);
欲了解更多详细信息,请参阅 此 StackOverflow 问题。
存储用户偏好设置
这是平台相关的。在 Windows Phone 8 中,您将使用 IsolatedStorageSettings。在 Windows 8 中,使用 ApplicationData.LocalSettings。您可以在 Superjumper 应用程序的 Settings 类中找到一个示例。
读取和写入文件
以下方法摘自 FruitCatcher 的 HighScoreManager 类,演示了如何在 Windows Phone 8 平台的文件中序列化和反序列化数据。Windows 8 的代码非常相似。
private async Task RetrieveHighScores() {
try
{
StorageFolder localFolder = ApplicationData.Current.LocalFolder;
StorageFile file = await localFolder.GetFileAsync(SCORES_DATA_FILE);
var inStream = await file.OpenStreamForReadAsync();
DataContractSerializer serializer =
new DataContractSerializer(typeof(List<HighScore>));
HighScores = (List<HighScore>)serializer.ReadObject(inStream);
inStream.Close();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private async Task Persist() {
try
{
StorageFolder localFolder = ApplicationData.Current.LocalFolder;
StorageFile file = await localFolder.CreateFileAsync(SCORES_DATA_FILE,
CreationCollisionOption.ReplaceExisting);
var outStream = await file.OpenStreamForWriteAsync();
DataContractSerializer serializer =
new DataContractSerializer(typeof(List<HighScore>));
serializer.WriteObject(outStream, HighScores);
await outStream.FlushAsync();
outStream.Close();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
如果您在 libgdx 中使用过 JSON 序列化,那么这个过程对您来说应该很熟悉。为了使上面的代码能够工作,您首先必须使用 `DataContract` 和 `DataMember` 属性注解 `HighScore` 类。对于 Java 开发人员来说,可能难以理解的一件事是使用 async 和 await 进行异步编程。在 Windows Phone 和 Windows 中,对于任何长时间运行的任务(文件访问、网络),使用异步方法是强制性的。我建议您在使用这两个关键字之前学习它们的工作原理。
生命周期事件
正如我已经说过,MonoGame 不支持生命周期事件。您需要自己在计划支持的每个平台中添加这些事件。您可以在 FruitCatcher 应用程序中找到如何在 Windows Phone 8 中实现此功能的一个示例。所需的是利用 App.xaml.cs 文件中提供的事件。
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (Game != null)
{
Game.Resume();
}
}
// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
if (Game != null)
{
Game.Pause();
}
}
// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
if (Game != null)
{
Game.Dispose();
}
}
在 `App` 类中,我添加了一个 `Game` 属性,它持有对已实现游戏的引用。在这个游戏中,我添加了 `Resume`、`Pause` 和 `Dispose` 方法。这些方法具有与 libgdx 中对应方法相似的含义。在创建游戏的 `GamePage.xaml.cs` 中,我将引用传递给 `App` 类。
_game = XamlGame<FruitCatcherGame>.Create("", this);
((App) App.Current).Game = _game;
欲了解更多详情,您可以查阅 FruitCatcher 的源代码。通常,MonoGame 与原生代码的集成并不困难,但您应该花一些时间学习 XAML 和平台内部结构。
调试日志
您可以使用 `System.Diagnostics.Debug.WriteLine()` 向控制台写入调试输出。好处是这个方法支持字符串格式参数。
还有什么
上述列表绝不完整。我计划将来添加更多内容。然而,我也觉得有必要提一下不支持的内容。如果您在项目中使用过 Scene2d,您会发现很难将其移植到 MonoGame。当然,MonoGame 不支持它,而且自己进行迁移会非常困难。此外,这里没有提到的是 3D 游戏。
用例
正如我在文章开头承诺的那样,不少于三个 libgdx 游戏已转换为 MonoGame。对于我的第一次游戏移植,我当然花费了大量时间。这是因为我需要发现 MonoGame 并开发移植技术。第二个和第三个游戏移植得非常快。我希望这篇文章和提供的源代码能帮助您快速迁移您的游戏。
青蛙过河

Frogger 是由 Roger Engelbert 移植到 libgdx 和其他游戏框架的简单游戏。我很乐意将其移植到 MonoGame。Windows Store 和 Windows Phone 8 版本均可用。
超级跳跃者
Super Jumper 是最著名的 libgdx 示例游戏之一。这是一款类似“涂鸦跳跃”的游戏,您通过跳跃平台引导 Bob 向上。在键盘系统中,您使用左右箭头键,而在手机上则使用加速度计。Super Jumper 是一个流行的示例,通常被库供应商选择来展示他们与 libgdx 的集成。因此,我相信它是一个很好的选择,可以演示从 libgdx 到 MonoGame 的移植。它还帮助我找到了一种方法来规避 MonoGame 中 2D 绘图缺少相机的问题。Windows Store 和 Windows Phone 8 版本均可用。
水果捕捉器

Fruit Catcher 是我创建并在此 Code Project 中展示的另一个简单的 libgdx 游戏。这是我首次尝试将 libgdx 游戏移植到 MonoGame,它也发布在Windows Phone Store。Fruit Catcher 没有使用前面描述的 LibgdxHelper 库,而是使用了类似的构造。然而,它是最完整的示例应用程序,所以我认为值得通读其代码。它特别演示了 MonoGame 如何与原生代码交互。它还集成了 Google 的 AdMob 广告。(然而,这些对我来说仍然无法正常工作。)只提供了 Windows Phone 8 版本。
摘要
这确实是一篇很长的文章,但我的目的是写一篇不仅仅是一次性阅读的文章,而是一篇也可以用作移植参考的文章。我真心希望您会觉得它有用,并且它会激励您将 libgdx 游戏移植到 MonoGame。祝您移植愉快!
历史
- 第一版:2014 年 4 月 14 日