Converting a 3D XNA 3.0 game to XNA 4.0 – an example

23 באוקטובר 2010

תגיות: , ,
4 תגובות

XNA 4.0 is great, but greatness comes at a cost. In order to have XNA 4.0 work uniformly across all its target platforms (Xbox, PC and Windows Phone 7) some breaking changes had to be made. While the changes improved the XNA framework by making things simpler (a lot of properties which didn’t really affect anything are gone) and better (the architecture makes more sense now, and it would be easy for XNA to support DirectX 10 at some point in the future), they also made it hard to port XNA 3.0 games to XNA 4.0. The main reason this task is so difficult is that there is no centralized location which catalogs all of the changes made and how they should be dealt with when porting a game. This is how this post was born.


I took one of the XNA 3.0 games available on the App Hub and converted it to XNA 4.0, documenting the various issues I overcame along the way. While this does not serve as a comprehensive guide as it is based on a single case, it does serve as the next best thing – a concrete example of how to get the job done.


I’ll just go ahead and say that the information to overcome most of these issues is spread about the web, namely on Shawn Hargreave’s blog, but now you don’t have to dig around for it.


So let us get started. Here are the main issues I have faced, along with their resolution.


Pixel shader 1.x is no longer supported:


All shaders contained in the project used Pixel Shader 1.x, which is no longer supported. The simple solution is to update all compilation targets to use Pixel Shader 2.0, though other options exist as detailed here.


For example, change the following:




       1: PixelShader = compile ps_1_1 ShipPS();

Which can be usually found in shader files (.fx files) to:




       1: PixelShader = compile ps_2_0 ShipPS();

This will prevent the following error: “error X3539: ps_1_x is no longer supported”.


Some shader effect states are now obsolete:


Certain aspects of the graphics state can no longer be controlled from within shaders, but are set in the game’s code instead. Specifically, I had to remove the following marked line from one of the shaders:



   1: technique Ship
   2: {
   3:    pass Single_Pass
   4:    {
   5:         ZENABLE = TRUE;
   6:         ZWRITEENABLE = TRUE;
   7:         ALPHATESTENABLE = FALSE;
   8:

Failure to do so will result in the error: “The effect state 'AlphaTestEnable' is obsolete and can no longer be used”. Whether or not you will have to compensate for such removed lines in code depends on XNA 4.0’s default states.


Have a look at this thread, which sheds a bit more light on the matter.


Old format XAP files need to be updated:


Most XNA 3.0 games contain a XAP file created with an old version of XACT. Simply open it with the version of XACT shipped with XNA 4.0 and save it again, that should do the trick. This will prevent the “Error 10 The .xap file was created with a version of XACT that is incompatible with the XNA Framework Content Pipeline version used by this project” error.


PresentationParameters has had a property overhaul:


Compare the previous version of the PresentationParameters class with the new one. As you can see, both classes are quite different, though some new properties are reminiscent of older ones (some are even identical). Each property has its own set of reasons for appearing, changing or disappearing and fixes have to be applied on a case by case basis.


In my specific case, I had to replace the creation of a render target which took in PresentationParameters.MultiSampleType and PresentationParameters.MultiSampleQuality to control antialiasing. Both properties are gone only to be replaced by the singular PresentationParameters.MultiSampleCount, which is now also an argument for the render target’s constructor. I will show a more concrete example in the next section.


The DepthStencilBuffer class no longer exists:


One of the breaking changes in XNA Game Studio 4.0 was the extermination of the DepthStencilBuffer class, for reasons detailed in this post. As stated in the post, the data previously contained in the DepthStencilBuffer class has been incorporated into the various render targets. This, combined with the necessary changes detailed in the previous section, caused the following bit of code:



   1: PresentationParameters pp = graphics.GraphicsDevice.PresentationParameters;
   2:  
   3: drawBuffer = new RenderTarget2D(graphics.GraphicsDevice,
   4:                                 FixedDrawingWidth, FixedDrawingHeight, 1,
   5:                                 SurfaceFormat.Color,
   6:                                 pp.MultiSampleType, pp.MultiSampleQuality);
   7:  
   8: drawDepthBuffer = new DepthStencilBuffer(graphics.GraphicsDevice,
   9:                                 FixedDrawingWidth, FixedDrawingHeight,
  10:                                 pp.AutoDepthStencilFormat,
  11:                                 pp.MultiSampleType, pp.MultiSampleQuality);
  12:  
  13: spriteBatch = new SpriteBatch(graphics.GraphicsDevice);

To turn into:



   1: PresentationParameters pp = graphics.GraphicsDevice.PresentationParameters;
   2:  
   3: drawBuffer = new RenderTarget2D(graphics.GraphicsDevice,
   4:                                 FixedDrawingWidth, FixedDrawingHeight,
   5:                                 true, SurfaceFormat.Color,
   6:                                 DepthFormat.Depth24Stencil8, pp.MultiSampleCount,
   7:                                 RenderTargetUsage.DiscardContents);
   8:  
   9: spriteBatch = new SpriteBatch(graphics.GraphicsDevice);

The member “drawDepthBuffer” appeared in other places in the code, and naturally must be removed.


GraphicsDevice no longer has a RenderState property:


One of the main reasons you would want to interact with the RenderState property is to properly mix 3D and 2D drawing as detailed in this post. To overcome this hurdle, just replace your code to match the updated post.


You would change code like this:



   1: IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   2:  
   3: graphicsService.GraphicsDevice.RenderState.AlphaTestEnable = false;
   4: graphicsService.GraphicsDevice.RenderState.AlphaBlendEnable = false;
   5: graphicsService.GraphicsDevice.RenderState.DepthBufferEnable = true;
   6: graphicsService.GraphicsDevice.RenderState.DepthBufferFunction = CompareFunction.LessEqual;
   7: graphicsService.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;

To:



   1: IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   2:  
   3: graphicsService.GraphicsDevice.BlendState = BlendState.Opaque;
   4: graphicsService.GraphicsDevice.DepthStencilState = DepthStencilState.Default;

VertexBuffer has been changed to be strongly typed:


Instead of wasting my “breath” (I am typing, after all), another post by Shawn Hargreaves says all there is to say about this change. This entails numerous changes to the project’s code, but following Shawn’s example should get you there. A specific example will be shown in the next section.


Effect and EffectPass classes have changed:


The main difference in Effect and EffectPass is that code no longer has to be enclosed between calls to Begin() and End(). Instead, EffectPass has a new Apply() method which you simply call whenever you wish to set the effect state onto the graphics device. Have a look over here, especially in the comments section.


Since examples always make things easier, not to say tangible, have a look at the following rendering method:



   1: public override void Render()
   2: {
   3:     IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   4:     GraphicsDevice device = graphicsService.GraphicsDevice;
   5:  
   6:     base.Render();
   7:  
   8:     device.VertexDeclaration = vertexDecl;
   9:     device.Vertices[0].SetSource(buffer, 0, VertexPositionColor.SizeInBytes);
  10:  
  11:     effect.Begin();
  12:     effect.Techniques[0].Passes[0].Begin();
  13:  
  14:     layer1TextureParam.SetValue(layer1);
  15:     layer2TextureParam.SetValue(layer2);
  16:     layer3TextureParam.SetValue(layer3);
  17:     layerFactorParam.SetValue(layerFactor);
  18:     layer1OffsetParam.SetValue(layer1Offset);
  19:     layer2OffsetParam.SetValue(layer2Offset);
  20:     effect.CommitChanges();
  21:  
  22:     device.DrawPrimitives(PrimitiveType.TriangleList, 0, xCount * yCount * 2);
  23:  
  24:     effect.Techniques[0].Passes[0].End();
  25:     effect.End();
  26: }

And now look at the updated version, which includes changes relating to VertexBuffer as well as Effect/EffectPass:



   1: public override void Render()
   2: {
   3:     IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   4:     GraphicsDevice device = graphicsService.GraphicsDevice;
   5:  
   6:     base.Render();
   7:  
   8:     device.SetVertexBuffer(buffer);
   9:     
  10:     layer1TextureParam.SetValue(layer1);
  11:     layer2TextureParam.SetValue(layer2);
  12:     layer3TextureParam.SetValue(layer3);
  13:     layerFactorParam.SetValue(layerFactor);
  14:     layer1OffsetParam.SetValue(layer1Offset);
  15:     layer2OffsetParam.SetValue(layer2Offset);
  16:  
  17:     effect.Techniques[0].Passes[0].Apply();
  18:  
  19:     device.DrawPrimitives(PrimitiveType.TriangleList, 0, xCount * yCount * 2);
  20: }

SpriteBatch.Begin(…) now takes in different parameters:


This change isn’t all that hard to get around, since enough plowing through MSDN (specifically here and here), combined with the similarly named arguments, should make it easy to translate your old calls to new ones. For example, the following call:



   1: SpriteBatch.Begin(SpriteBlendMode.None);

Turns into this:



   1: SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque);

ModelMesh and ModelMeshPart had some properties changed:


Much like PresentationParameters mentioned earlier, these two classes have had some properties changed, and by changed I also mean removed/added. Have a look at the 3.1 MSDN pages (ModelMesh, ModelMeshPart) and the new 4.0 pages (ModelMesh, ModelMeshPart). By way of example, the following code (where modelMesh is a ModelMesh and meshPart is a ModelMeshPart):



   1: if (meshPart.PrimitiveCount > 0)
   2: {
   3:     IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   4:     GraphicsDevice gd = graphicsService.GraphicsDevice;
   5:  
   6:     gd.VertexDeclaration = meshPart.VertexDeclaration;
   7:     gd.Vertices[0].SetSource(modelMesh.VertexBuffer, meshPart.StreamOffset, meshPart.VertexStride);
   8:     gd.Indices = modelMesh.IndexBuffer;
   9:  
  10:     gd.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshPart.BaseVertex, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
  11: }

Turns into something slightly different (this also contains changes made to work with the new VertexBuffer):



   1: if (meshPart.PrimitiveCount > 0)
   2: {
   3:     IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)GameInstance.Services.GetService(typeof(IGraphicsDeviceService));
   4:     GraphicsDevice gd = graphicsService.GraphicsDevice;
   5:  
   6:     gd.SetVertexBuffer(meshPart.VertexBuffer);
   7:     gd.Indices = meshPart.IndexBuffer;
   8:  
   9:     gd.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshPart.VertexOffset, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
  10: }

Point sprites are a thing of the past:


There is a pretty detailed post about this issue on Shawn Hargreave’s blog, which says more than I could hope to say about the matter.


One of the alternatives to point sprites, which I adopted, is rendering tiny triangles. Just be sure not to use the exact same point for all of the triangle’s vertices, as nothing will be rendered that way. Instead, make sure that all vertices are slightly different.



Texture content processor now has a “Premultiply Alpha” property:


This point is easy to overlook and caused me quite a bit of grief. XNA 4.0’s texture content processor (the one used by default for images) has a new “Premultiply Alpha” property which is on by default. This was not the default in earlier versions of XNA, which caused a plethora of baffling rendering issues. If your texture does not expect this, simply turn it off.


State objects are read only:


This is not strictly related to the conversion process, but is something to be aware of nonetheless. The GraphicDevice’s various state objects, such as GraphicsDevice.RasterizerState or GraphicsDevice.DepthStencilState are read only.
If you try to do something like GraphicsDevice.DepthStencilState.DepthBufferWriteEnable = true; you will be greeted by the following InvalidOperationException: “Cannot change read-only DepthStencilState. State objects become read-only the first time they are bound to a GraphicsDevice. To change property values, create a new DepthStencilState instance.”.

Instead, just do what the exception suggests and write something like:
GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
For a read-only depth stencil. You could also define your own state objects and assign them, just be sure to cache the objects instead of creating them repeatedly.

In retrospect, I could have named this post “A guided tour of Shawn Hargreave’s blog”. Hopefully this information, bolstered by some concrete example, will help you in your game converting endeavors.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

4 תגובות

  1. serdar15 בנובמבר 2010 ב 15:51

    I download some demo files samples and could not convert them.

    code has this line.

    spriteBatch.Draw(texture, centerTexture, new Color(Color.White, Alpha));

    I changed new Color(Color.White, Alpha) to Color.White…

    but how can I change this?

    new Color(titleBG, titleOpacity * TransitionPercent)

    להגיב
  2. grozen15 בנובמבר 2010 ב 16:43

    Hello serdar,

    First off, let me just point out that "Color.White" is not necessarily the same as "Color(Color.White, Alpha)". In the second case, Alpha can be any value between 0.0 and 1.0, and it will be equivalent to "Color.White" only when it is 1.0 (fully opaque).
    A better approach would have been to turn that line into "Color.White * Alpha" which would result in a white color with an alpha level of "Alpha".

    This can also help you with the second case. Assuming "titleBG" is also a color, you would simply turn "Color(titleBG, titleOpacity * TransitionPercent)" into "titleBG * (titleOpacity * TransitionPercent)".

    להגיב
  3. Roy Triesscheijn8 בדצמבר 2010 ב 17:35

    In this code sample you use meshPart.VertexOffset as your 'meshPart.BaseVertex', but are you sure this is the same thing?

    להגיב
  4. grozen8 בדצמבר 2010 ב 18:27

    Pretty sure, yes.
    The documentation (http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.modelmeshpart.basevertex%28XNAGameStudio.31%29.aspx and http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.graphics.modelmeshpart.vertexoffset.aspx) seems to indicate that these two are the same thing indeed (the wording is a little tricky to compare).
    Also, matter-of-factly, the converted game worked just like the original one.

    להגיב