×
Namespaces

Variants
Actions

MonoGame on Windows Phone – A Tutorial on Building Levels for 3D games

From Nokia Developer Wiki
Jump to: navigation, search

In this article, we will take a look at some options for building and displaying 3D geometry for levels in 3D games built with MonoGame.

WP Metro Icon Joystick.png
WP Metro Icon UI.png
WP Metro Icon DirectX.png
WP Metro Icon WP8.png
SignpostIcon WP7 70px.png
Article Metadata
Code ExampleCompatibilityArticle
Created: this_is_ridiculous (10 May 2013)
Last edited: hamishwillee (03 Jul 2013)

Contents

Introduction

When it comes to building a 3D game, every developer asks himself how will he build and represent levels in it. You think of this even more when it comes to the team who will build those levels. In this article, we will take a look at some of the options and investigate one of them in detail. This article is divided into two parts. In the first part we will build a level concept and the second part brings this level to XNA and MonoGame projects for WP7 and WP8 respectively.

Options for level design

There are several options for level design.

The first is the most straightforward: your level is a 3D model. This approach is good when the level geometry is fairly simple, non-destructible and the level itself is not too large to consume most of 33 or 17 milliseconds available for you per frame. However when it comes to large levels this comes to be not the most efficient way

The second option is procedural level generation. It’s good if your team consists mostly of programmers and not designers. So part of your team can take upon creating algorithms and some code for just generating levels suitable for your games.

The third is to develop a custom level editor and the runtime code to load the level and draw it to the screen. That option is OK in case your team is big enough to have dedicated to the tool development people. This also requires fair enough complex system underlying the level runtime for this to as efficient as possible according to your team’s abilities and knowledge. The bad thing about taking this path is you will spend more time creating and fixings bugs in the editor itself than developing a game.

The fourth option, and the one we discuss in this article, is to use a third-party editor. There are many different editors available out there. However we will take a look at Leadwerks’ 3D World Studio.

What is this 3D World Studio?

As the official page states 3D World Studio is the map editor of choice for use with DarkBASIC Professional, Torque Game Engine, Blitz3D, and other 3D engines. The program is ideal for game environments, 3D presentations and commercial applications. Sounds cool! It also features a very similar to UnrealEd UI and interaction model. Some of the other features are:

  • Static lights using light maps.
  • Light maps are embedded inside the output file.
  • Support for .X meshes importing and exporting.
  • Terrains.
  • Custom shapes and prefabs.
  • Support entities for lights, spawn points, triggers, etc.
  • Entities extensibility.

You can read more about the features at the official web page of the editor. http://www.thegamecreators.com/?m=view_product&id=2100

Ok, Ok, I’m Impressed, how do I create those levels?

And here starts the interesting part. When you start the 3D World Studio you will see something similar to:

3D World Studio

In the middle you have 4 viewports as in any regular 3D package. At the right hand side of window you have the library with categories for primitives and textures. And at the top of window there is a toolbar. Despite of this pretty Spartan interface the application can produce pretty much nice levels with backed static lights. So let’s start.

Part One - Building a Box

From Object category select Solids, then select box from Objects dropdown and start a box in a Top – X/Z viewport.

We start with a box

Notice 3 red cubes buttons at the top toolbar.

Manupulation modifiers

These are manipulation modifiers. The first from the left is a brushes (objects) modifier. When selected you will be picking the whole objects like the box we’ve just created. The middle one is a face selection modifier. This will select faces (polygons). And the one from the right is a point (vertex) manipulation modifier. When selected allows you to manipulate with vertices of a brush.

The Box is Cool However Building a House of It Would be Way Cooler

Notice the four gray icons on the toolbar.

Geometry editing tools

These are (from the left) Carve, lets you slice one piece of geometry with another one. Hollow, lets you make hollows in geometry like building the entire room with walls from just a box. Slice Plane lets you slice the brush by using a cutting plane into two pieces. And the last one is Extrude Face which actually lets you extruding faces.

Now when we are familiar with the tools let’s build a house out of the box. First let’s slice the thin top part of our box with Slice Plane to prepare geometry for the roof.

The Roof - alpha version

To make our slicing more accurate we can change the grid step in each of viewports independently. For this left-click the desired viewport use the [ and ] keyboard keys to increase or decrease grid step. To switch the viewport to full app space use the 'F12 key.

The Roof - alpha version


So after around 15 minutes I was able to produce this:

House alpha version

What’s left is adding windows, doors and lights. For this we can use one single cube of a uniform size to carve out the windows. Also we will need a texture for windows. If we want the lights to come through the windows the textures should be in PNG format with alpha value somewhere around 128 (or 0.5 depending on the image editor you use). In order to add your textures to 3D World Studio library you will have open installation directory of 3DWS in Windows Explorer and locate Materials Folder. Open it and create a subfolder with any name you like and place the texture there. After restart of 3DWS you will see your new texture category and textures inside it. One should be very patient when building levels or level geometry for prefabs. This process takes time and large amount of try-fails. So it’s really smart to save your progress often.

When we have our windows, doors and lamps in place, let’s add some lights.

For adding lights select Entities from Object Categories and lights from Objects dropdown. I’ve placed some inside of the house to make use of windows transparency and in each of outdoor lamps. You can multi-select any type of object inside the scene. To open object properties hit Shift-Enter keys combination. For lights there are special built in fields that correspond to light properties. From there you can control light intensity, linear falloff, light color and range. To render lights click the light bulb button on the toolbar. This will open Lights Rendering properties.

Render Lights Dialog

From there you can select Ambient light color, lightmap texture size, blur radius, etc. So hit the Render button and wait for job done message. The wait time depends on the scene complexity (brushes and lights count), Lightmap size, terrains and models (imported .X meshes) presence inside the scene. When rendering is done to see the result you should switch view mode to Textured+Lighting. And the results are:

Without lights
With lights

Part Two - Bring this to the game

First off you should download the 3D World Studio runtime and Content Pipeline Extension in order to build this stuff. If you target Windows Phone 7.x with native XNA you should be fine with that runtime only. However if you target Windows Phone 8.0 and you want 8.0 features that are not accessible within XNA (like custom shaders) you should rebuild the runtime library in order to load the level. Tools we’ll need for this:

  • Visual Studio Express For Windows Phone 7.1
  • Visual Studio Express for Windows Phone 8.0
  • 3D World Studio XNA Content Pipeline Extension (https://xnawp7levelpipeline.codeplex.com/ or you can download the updated version at the end of this article)

First download the runtime and open up the VS 2010 for WP 7.1, create new XNA project Add the assets to Content project. It’s good to keep the assets folder manageable so levels go to Levels folder, textures to Textures Folder.

  1. Add the GameLevelWindowsPhone project to the solution and add references to it from your project.
  2. Add the pipeline project to solution as well and reference it from Content Project.
  3. Add the test_level.3dws to Content\Levels folder. Ensure that correct Importer and Processor are selected.
  4. Copy the textures to Content\Textures folder but do not add those to the project.

If you try to build the solution at this point you will get an error that some surface XML file is missing. The surface file is the XML document that contains path to the corresponding to its name texture. This means that you will have to create as much of those surface XMLs as textures count you have added to the level while designing it in 3D World Studio. Those XML documents should be located at the same folder as your level is but should not be added to the project.

So now when our solution builds let’s code

Coding the Level

When new XNA project is created you have an almost empty Game1 class. First let’s add a using statement for GameLevel. Then let’s declare some class level variables we will be using while drawing. These are:

Matrix View, Projection;
Level level;

Now navigate to LoadContent method within Game1 and after creation of spriteBatch add this

level = Content.Load<Level>(@"Levels\test_level");
 
View = Matrix.CreateLookAt(new Vector3(-701, 261, 772), new Vector3(200f, 0f, 0f), Vector3.Up);
Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 800f / 480f, 1, 10000);

Next navigate to Draw(GameTime gameTime) method and replace it with the next code

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
 
foreach (Model model in level.Geometry)
{
LevelModelParams param = (LevelModelParams)model.Tag;
 
foreach (ModelMesh modelMesh in model.Meshes)
{
foreach (Effect effect in modelMesh.Effects)
{
if (effect is DualTextureEffect)
{
dualEffect = effect as DualTextureEffect;
 
dualEffect.View = View;
dualEffect.Projection = Projection;
dualEffect.World = Matrix.Identity;
dualEffect.DiffuseColor = Vector3.One / 1.5f;
 
if (param.lightMapIndex > -1)
{
dualEffect.Texture2 = level.Lightmaps[param.lightMapIndex];
}
}
else if (effect is BasicEffect)
{
((BasicEffect)effect).View = View;
((BasicEffect)effect).Projection = Projection;
((BasicEffect)effect).World = Matrix.Identity;
}
else if (effect is AlphaTestEffect)
{
((AlphaTestEffect)effect).View = View;
((AlphaTestEffect)effect).Projection = Projection;
((AlphaTestEffect)effect).World = Matrix.Identity;
}
 
foreach (EffectPass p in effect.CurrentTechnique.Passes)
{
p.Apply();
}
}
 
modelMesh.Draw();
}
}
 
base.Draw(gameTime);
}

After you build it and deploy to device or the emulator you will see something like this:

WP7 XNA Result

This looks a bit different than the 3D World Studio renders. The thing is that the litghmap color is inverted. XNA Pipeline reads the values as RGB channels, however they are not RGB. It seems to be BGR. However fear not! We will fix it with rendering the lights again after inverting two bytes of colors for light entities when everything in our level is said and done.

After light color inverted in 3D World Studio
After light color inverted in XNA game

Now let’s do the same but with MonoGame

For this we need the MonoGame library itself and the latest SharpDX binaries which can be found at the first post here.

So our strategy will look like:

  1. Download both libraries and extract them any location you like
  2. Navigate to “…\MonoGame-wp8_working\ProjectTemplates\VisualStudio2012\WindowsPhone” directory select all files and add to zip archive. Copy this archive to “…\Documents\Visual Studio 2012\Templates\ProjectTemplates\Visual C#”
  3. Start Visual Studio 2012 for Windows Phone 8 and under C# templates you will see a new template
    WP8 MonoGame Template
  1. Create a new project from this template. I’ve named my project TestLevel
  2. Create a C# Class Library project for Windows Phone with the same name as it was for XNA. In my case it’s GameLevel.
  3. Add MonoGame.WindowsPhone project to your solution as an existing project and reference it from your MonoGame game and GameLevel projects. (At this you may have to read references to SharpDX binaries for MonoGame Framework project). For Emulator you should x86 binaries, for the actual device you should add ARM binaries.
  4. In your game project create a folder named “Content” and copy Textures and Levels folder with XNBs to that folder. Include all of them to your project. Then set compile action to “None” and copy to output action to “Copy if newer” for these files
  5. Build you solution.

At this point you may get a build error stating about namespace conflicts. This happens because of WindowsPhone library that you can find in references at any C# Windows Phone 8 project includes libraries for Microsoft XNA. And we do not need those. To fix this error open location of your GameLevel class library, locate GameLevel.csproj file and open it in any text editor you like. I use Notepad++ for this. Scroll down and find the next lines:

<Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).$(TargetFrameworkVersion).Overrides.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).CSharp.targets" />

Right after these lines add the next:

<Target Name="MonoGame_RemoveXnaAssemblies" AfterTargets="ImplicitlyExpandTargetFramework">
<Message Text="MonoGame - Removing XNA Assembly references!" Importance="normal" />
<ItemGroup>
<ReferencePath Remove="@(ReferencePath)" Condition="'%(Filename)%(Extension)'=='Microsoft.Xna.Framework.dll'" />
<ReferencePath Remove="@(ReferencePath)" Condition="'%(Filename)%(Extension)'=='Microsoft.Xna.Framework.GamerServices.dll'" />
<ReferencePath Remove="@(ReferencePath)" Condition="'%(Filename)%(Extension)'=='Microsoft.Xna.Framework.GamerServicesExtensions.dll'" />
<ReferencePath Remove="@(ReferencePath)" Condition="'%(Filename)%(Extension)'=='Microsoft.Xna.Framework.Input.Touch.dll'" />
<ReferencePath Remove="@(ReferencePath)" Condition="'%(Filename)%(Extension)'=='Microsoft.Xna.Framework.MediaLibraryExtensions.dll'" />
</ItemGroup>
</Target>

and save the file. This will tell Visual Studio to exclude mentioned libraries from our project.

When you come back to Visual Studio it will prompt that project file has been changed in external editor and will ask if it should reload the project. Agree to the prompt.

In Solution Explorer under your game project locate Game1.cs and open it.

We will need some class level variables for actual drawing and some variable to handle landscape screen orientation. Let’s declare those:

Level level;
RenderTarget2D finalImageTarget;
// we will stick to WVGA resolution for this tutorial
public static Vector2 screenSize = new Vector2(800f, 480f);
Vector2 halfScreen;
Vector2 invHalfScreen;
Matrix View, Projection;
// these four variables needed to save and restore the
// GraphicsDevice state before and after drawing sprites with spritebatch
private BlendState _preSpriteBlendState = BlendState.Opaque;
private DepthStencilState _preSpriteDepthStencilState = DepthStencilState.Default;
private RasterizerState _preSpriteRasterizerState = RasterizerState.CullCounterClockwise;
private SamplerState _preSpriteSamplerState = SamplerState.LinearWrap;

Now locate LoadContent() method and after creation of _spriteBatch object add next few lines:

finalImageTarget = new RenderTarget2D(GraphicsDevice, 800, 480, false, SurfaceFormat.Color, DepthFormat.Depth16);
 
halfScreen = screenSize / 2;
invHalfScreen = new Vector2(halfScreen.Y, halfScreen.X);
level = Content.Load<Level>("Levels/test_level");
View = Matrix.CreateLookAt(new Vector3(1191, 61, 1272), new Vector3(1091, 1, 1152), Vector3.Up);
Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 800f / 480f, 1, 10000);

Next locate the Draw() method and replace it with

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.SetRenderTarget(finalImageTarget);
GraphicsDevice.Clear(Color.CornflowerBlue);
 
SamplerState s = new SamplerState();
s.AddressU = TextureAddressMode.Wrap;
s.AddressV = TextureAddressMode.Wrap;
 
GraphicsDevice.SamplerStates[0] = s;
 
foreach (Model model in level.Geometry)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (Effect effect in mesh.Effects)
{
if (effect is DualTextureEffect)
{
((DualTextureEffect)effect).World = Matrix.Identity;
((DualTextureEffect)effect).View = View;
((DualTextureEffect)effect).Projection = Projection;
 
LevelModelParams param = (LevelModelParams)model.Tag;
if (param.lightMapIndex > -1)
{
((DualTextureEffect)effect).Texture2 = level.Lightmaps[param.lightMapIndex];
}
}
else if (effect is BasicEffect)
{
((BasicEffect)effect).View = View;
((BasicEffect)effect).Projection = Projection;
((BasicEffect)effect).World = Matrix.Identity;
}
else if (effect is AlphaTestEffect)
{
((AlphaTestEffect)effect).View = View;
((AlphaTestEffect)effect).Projection = Projection;
((AlphaTestEffect)effect).World = Matrix.Identity;
}
 
foreach (EffectPass p in effect.CurrentTechnique.Passes)
{
p.Apply();
}
}
 
mesh.Draw();
}
}
 
GraphicsDevice.SetRenderTarget(null);
StoreStateBeforeSprites();
spriteBatch.Begin();
spriteBatch.Draw(finalImageTarget, invHalfScreen, null, Color.White, MathHelper.PiOver2, halfScreen, Vector2.One, SpriteEffects.None, 0f);
spriteBatch.End();
RestoreStateAfterSprites();
 
base.Draw(gameTime);
}

We also need two functions mentioned in Draw method to handle GraphicsDevice states

/// <summary>
/// Store all of the graphics device state values that are modified by
/// the sprite batch. These can be restored by later calling the
/// RestoreStateAfterSprites function.
/// </summary>
public void StoreStateBeforeSprites()
{
_preSpriteBlendState = GraphicsDevice.BlendState;
_preSpriteDepthStencilState = GraphicsDevice.DepthStencilState;
_preSpriteRasterizerState = GraphicsDevice.RasterizerState;
_preSpriteSamplerState = GraphicsDevice.SamplerStates[0];
}
 
/// <summary>
/// Restore all of the graphics device state values that are modified by
/// the sprite batch to their previous values, as saved by an earlier call to
/// StoreStateBeforeSprites function.
/// </summary>
public void RestoreStateAfterSprites()
{
GraphicsDevice.BlendState = _preSpriteBlendState;
GraphicsDevice.DepthStencilState = _preSpriteDepthStencilState;
GraphicsDevice.RasterizerState = _preSpriteRasterizerState;
GraphicsDevice.SamplerStates[0] = _preSpriteSamplerState;
}

So now we are pretty much ready to build and see what we’ve got so far.

WP8 MonoGame result

Warning and notes

First and the most important is that at the moment 3D World Studio does not function correctly on Windows 8 OS. So you will have to run Windows 7 or earlier in order to build levels in it

Second is that it is good to keep in mind Color values are not RGB but are closer to BGR vales. Inverting lights colours before final lights rendering does a good job.

The third one is that it is good to group your geometry by name from properties window. Objects with the same name will be treated as the single Model so this can influence correctness of building collision geometry

Lastly, 3D World Studio is not free - at time of writing it costs about $50 USD.

What's Next?

  • You can throw in some descent camera code.
  • You can subclass the single object from the models to test it for visibility or even add Octree code to query the visible to your camera objects, so your loops will become shorter.
  • You can add physics library for collisions and real world physics laws

Summary

The static lights or the lightmaps can add much depth to our games practically at no cost so we can spend the devices' GPU horsepower for animations, skinning, particle effects and post-processing. The complexity of static lights depends only on how much time you are eager to wait for them to render. We've seen so far how we can build levels for our XNA/MonoGame games using 3D World Studio. After spending some time with the editor you can feel pretty comfortable with it and I think you will like it.

For only 50 US dollars it’s a decent software to use for building cities, dungeons and warfare fields for your games.

The Code

The Windows Phone 7.x XNA Solution. Requires Windows Phone 7.1 SDK. This code was tested on HTC 7 Mozart, HTC HD7, HTC 8s, Nokia Lumia 800 and 710, Samsung Omnia 7.

The Windows Phone 8.0 MonoGame Solution. Requires Windows Phone 8.0 SDK. This was tested on Nokia Lumia 820 and HTC 8s

3D World Studio Version 5.52 was used to create a level.

if you'd like to see a more advanced code for using 3D World Studio levels in XNA please refer to this url http://sdrv.ms/10gjwZV It's the game that me and my friend created during the 24 hour WP7 hackathon in Lviv, Ukraine in October of 2011. It's XNA only project and here's the video of what it looks like http://sdrv.ms/10ggVzd

Hardware used for testing: Pure XNA Solution will work on any Windows Phone 7 Device. However the overall performance will depend on the complexity of the scene being drawn. So if you'd like to have big levels built with 3D World Studio you should keep in mind some sort of optimizations will be needed. This will vary from geometry complexity and texture sizes to the algorithms for sorting and batching the geometry in order to keep draw calls amount as low as possible.

MonoGame Solution will also work on any Windows Phone 8 device, but exactly this code will need some refactoring for correct drawing on devices with HD screens. This could be as simple as scaling the finalImageTarget RenderTarget to fill the entire screen. Performance-wise I can only recommend batched drawing with any kind of visibility testing: from simple at the very beginning of Draw() function

if(!camera.Frustum.Contains(levelElement.BoundingBox)) // or any other bounding volume that fits your needs
return;

to Octree geometry culling with Occlusion Qery and dynamic LOD algorithms.

Enjoy! :)

This page was last modified on 3 July 2013, at 05:31.
274 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×