10/21/07

VB.NET XNA Tutorial 4: Starting an engine, and the VBContentManager

[GSE 1.0 Refresh]

Here's the link to Tutorial 4: Starting an engine on Vimeo.

This tutorial is a little bit longer and a little bit more involved than the past tutorials. We'll start down the road to separating game logic from game logistics by creating a new project called XNA 2D Engine and moving the "MyGame" class into it. We'll turn the MyGame into a base class to be used with the 2D engine.

We'll introduce the VBContentManager class, my implementation of a ContentManager extended with our "VB Content Pipeline hack" code (there's lots more on that, below). We'll then go on to making a primitive sprite in Photoshop and drawing it to the screen with a SpriteBatch object.

Here is some companion text to go with the video, that describes in detail some code I'll be importing but not spending a lot of time describing in the video:



In a standard C# XNA project, the default game class is created for you with a GraphicsDeviceManager and a ContentManager both instantiated and ready to go. As we saw in Tutorial 3, it's fairly easily make our own class that inherits from the base XNA Game class, create our own GraphicsDeviceManager and quickly reproduce what the C# template generates automatically.

What tutorial 3 did not touch on was the ContentManager, because for us VB'ers, the ContentManager is really the point where the lack of VB support in XNA requires us to do a little bit of legwork, so I held off on the subject until now.

In C#, the ContentManager class loads cross-platform file types (like .xnb files) that MSBuild creates automatically from things like .png or .jpg files that get included in the C# project. The primary issue when using a ContentManager with VB is getting these .xnb files created from our .png or .jpg files, because there is no automatic conversion process built into the IDE.

My approach to solving this problem was to borrow largely what's been documented by Alan Phipps, and from that, create my own ContentManager class called the VBContentManager class. Here's is the VBContentManager class, in it's entirety:

Public Class VBContentManager
 Inherits ContentManager

 Public Fonts As Hashtable(Of SpriteFont)
 Public Textures As Hashtable(Of Texture2D)
 Public SubTextures As Hashtable(Of Hashtable(Of Rectangle))
 Public Sounds As Hashtable(Of SoundBank)

 Private strFakeCSharpProject As String
 Private strContentRootFolder As String
 Private strPipelineRootFolder As String
 Private objContent As List(Of ContentFile)
 Private intDupeSound, intDupeTexture, intDupeFont As Integer

 Public Enum ContentTypes
 Textures = 0
 Fonts = 1
 Sounds = 2
 End Enum

 Private Enum ImporterName
 TextureImporter = 0
 FontDescriptionImporter = 1
 XactImporter = 2
 End Enum

 Private Enum ProcessorName
 SpriteTextureProcessor = 0
 FontDescriptionProcessor = 1
 XactProcessor = 2
 End Enum

 Public Structure ContentFile

 Public ContentType As ContentTypes
 Public File As System.IO.FileInfo
 Public Name As String

 Public Sub New(ByVal ContentType As ContentTypes, ByVal ContentFile As System.IO.FileInfo, ByVal Name As String)
 Me.ContentType = ContentType
 Me.File = ContentFile
 Me.Name = Name
 End Sub

 End Structure

 ''' <summary>
 ''' Creates a new ContentManager which has been extended with functionality to support a Visual Basic (or other non-C#) project
 ''' </summary>
 ''' <param name="ServiceProvider">Your game has a .Services property; send that in here.</param>
 ''' <param name="ContentRootFolder">The root of your content folder, relative to your projects bin\Debug folder.</param>
 ''' <remarks></remarks>
 Public Sub New(ByVal ServiceProvider As IServiceProvider, ByVal ContentRootFolder As String)

 MyBase.New(ServiceProvider)

 Me.strContentRootFolder = ContentRootFolder
 Me.strPipelineRootFolder = ContentRootFolder.Replace("../""")

 Me.Fonts = New Hashtable(Of SpriteFont)
 Me.Textures = New Hashtable(Of Texture2D)
 Me.SubTextures = New Hashtable(Of Hashtable(Of Rectangle))
 Me.Sounds = New Hashtable(Of SoundBank)

 End Sub

 ''' <summary>
 ''' Load all assets that have been compiled by a VBContentManager.
 ''' </summary>
 ''' <param name="Compile">Whether or not to have MSBuild compile your game content before attempting to load it</param>
 ''' <remarks></remarks>
 Public Sub LoadPipeline(ByVal Compile As Boolean)

 If Compile Then
 Me.CompileContent()
 End If

 For Each obj As ContentFile In Me.objContent

 Try

 Select Case obj.ContentType

 'The pipeline will be compiled into either debug or release, depending upon
 'which option is chosen in the IDE.  We don't want to use the My namespace
 'though, because it does not work with the xbox 360.

 Case ContentTypes.Fonts

 With obj.File.FullName.ToLower
 Me.Fonts.Add(obj.Name, Me.Load(Of SpriteFont)(.Substring(.LastIndexOf("\" & Me.strPipelineRootFolder.ToLower) + 1).Replace(obj.File.Extension.ToLower, "")))
 End With

 Case ContentTypes.Sounds

 With obj.File.FullName.ToLower
 Me.Sounds.Add(obj.Name, Me.Load(Of SoundBank)(.Substring(.LastIndexOf("\" & Me.strPipelineRootFolder.ToLower) + 1).Replace(obj.File.Extension.ToLower, "")))
 End With

 Case ContentTypes.Textures

 With obj.File.FullName.ToLower
 Me.Textures.Add(obj.Name, Me.Load(Of Texture2D)(.Substring(.LastIndexOf("\" & Me.strPipelineRootFolder.ToLower) + 1).Replace(obj.File.Extension.ToLower, "")))
 End With

 End Select

 Catch ex As Exception

 'msgBox("While loading the pipeline, a piece of content called " & obj.Name & " could not be found, and was skipped")

 'A new piece of content has been added, but the programmer failed
 'to instruct the VBContentPipeline to compile it.  So the content
 'will not be available during this instance of the Game.

 End Try

 Next

 End Sub

 Private Sub CompileContent()

 Dim objProcess As Process

 Me.objContent = New List(Of ContentFile)

 Me.HarvestContent()

 If Me.objContent.Count > 0 Then

 Me.StartTheFakeCSharpFile()

 Me.AddContentToTheFakeCSharpFile()

 Me.EndTheFakeCSharpFile()

 Try 'to write the fake C# project file to disk

 Using TempStreamWriter As System.IO.StreamWriter = New System.IO.StreamWriter(Me.strContentRootFolder & "\VBContentPipeline.csproj"False)

 With TempStreamWriter

 .Write(Me.strFakeCSharpProject)
 .Close()

 End With

 End Using

 Try 'to shell out to MSBuild.exe, sending it our fake c# project file

 objProcess = System.Diagnostics.Process.Start("""" & Environment.GetEnvironmentVariable("Windir") & _
 "\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe""""""" & Me.strContentRootFolder & _
 "\VBContentPipeline.csproj"" /verbosity:d /l:FileLogger,Microsoft.Build.Engine;logfile=""" & _
 Me.strContentRootFolder & "\VBPipelineBuild.log""")

 'wait for that to finish

 While Not objProcess.HasExited
 Threading.Thread.Sleep(100)
 End While

 Catch ex As Exception

 MsgBox("When building the pipeline and trying to run MSBuild.exe, " & ex.Message)

 End Try

 Catch ex As Exception

 MsgBox("When building the pipeline and trying to create the fake C# file """ & Me.strContentRootFolder & "\VBContentPipeline.csproj"", " & ex.Message)

 End Try

 End If

 End Sub

 Private Sub HarvestContent()

 Dim objRoot As System.IO.DirectoryInfo = New System.IO.DirectoryInfo(Me.strContentRootFolder)

 For Each sound As System.IO.FileInfo In objRoot.GetFiles("*.xab", IO.SearchOption.AllDirectories)
 If Me.objContent.Contains(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, ""))) Then
 Me.intDupeSound += 1
 Me.objContent.Add(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, "") & Me.intDupeSound.ToString))
 Else
 Me.objContent.Add(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, "")))
 End If
 Next

 For Each texture As System.IO.FileInfo In objRoot.GetFiles("*.png", IO.SearchOption.AllDirectories)
 If Me.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then
 Me.intDupeTexture += 1
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeSound.ToString))
 Else
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "")))
 End If
 Next

 For Each texture As System.IO.FileInfo In objRoot.GetFiles("*.jpg", IO.SearchOption.AllDirectories)
 If Me.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then
 Me.intDupeTexture += 1
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeSound.ToString))
 Else
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "")))
 End If
 Next

 For Each texture As System.IO.FileInfo In objRoot.GetFiles("*.gif", IO.SearchOption.AllDirectories)
 If Me.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then
 Me.intDupeTexture += 1
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeSound.ToString))
 Else
 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "")))
 End If
 Next

 For Each font As System.IO.FileInfo In objRoot.GetFiles("*.spritefont", IO.SearchOption.AllDirectories)
 If Me.objContent.Contains(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, ""))) Then
 Me.intDupeFont += 1
 Me.objContent.Add(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, "") & Me.intDupeSound.ToString))
 Else
 Me.objContent.Add(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, "")))
 End If
 Next

 End Sub

 Private Sub StartTheFakeCSharpFile()

 Me.strFakeCSharpProject = _
 "<Project DefaultTargets=""Build"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">" & vbCrLf & _
 "<PropertyGroup>" & vbCrLf & _
 "<Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>" & vbCrLf & _
 "<Platform Condition="" '$(Platform)' == '' "">x86</Platform>" & vbCrLf & _
 "<OutputType>WinExe</OutputType>" & vbCrLf & _
 "<AppDesignerFolder>Properties</AppDesignerFolder>" & vbCrLf & _
 "<AssemblyName>" & My.Application.Info.AssemblyName & "</AssemblyName>" & vbCrLf & _
 "<XnaPlatform>Windows</XnaPlatform>" & vbCrLf & _
 "<XnaFrameworkVersion>v1.0</XnaFrameworkVersion>" & vbCrLf & _
 "<XNAGlobalContentPipelineAssemblies>Microsoft.Xna.Framework.Content.Pipeline.EffectImporter.dll;" & _
 "Microsoft.Xna.Framework.Content." & _
 "Pipeline.FBXImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.TextureImporter.dll;" & _
 "Microsoft.Xna.Framework.Content.Pipeline.XImporter.dll</XNAGlobalContentPipelineAssemblies>" & vbCrLf & _
 "</PropertyGroup>" & vbCrLf & _
 "<PropertyGroup Condition="" '$(Configuration)|$(Platform)' == 'Debug|x86' "">" & vbCrLf & _
 "<OutputPath>" & My.Application.Info.DirectoryPath & "\" & Me.strPipelineRootFolder & "</OutputPath>" & vbCrLf & _
 "</PropertyGroup>" & vbCrLf & _
 "<ItemGroup>"

 End Sub

 Private Sub AddContentToTheFakeCSharpFile()

 For Each content As ContentFile In Me.objContent

 Me.strFakeCSharpProject &= vbCrLf & _
 "<Content Include=""" & content.File.FullName & """>" & vbCrLf & _
 "<XNAUseContentPipeline>true</XNAUseContentPipeline>" & vbCrLf & _
 "<Importer>" & CType(content.ContentType, ImporterName).ToString & "</Importer>" & vbCrLf & _
 "<Processor>" & CType(content.ContentType, ProcessorName).ToString & "</Processor>" & vbCrLf & _
 "<Name>" & content.Name & "</Name>" & vbCrLf & _
 "</Content>"
 Next

 End Sub

 Private Sub EndTheFakeCSharpFile()

 Me.strFakeCSharpProject &= _
 "</ItemGroup>" & vbCrLf & _
 "<Import Project=""$(MSBuildBinPath)\Microsoft.CSharp.targets"" />" & vbCrLf & _
 "<Import Project=""$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.ContentPipeline.targets"" />" & vbCrLf & _
 "<Import Project=""$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.Common.targets"" />" & vbCrLf & _
 "</Project>"

 End Sub

End Class


In a nutshell, the VBContentManager class uses a supplied directory as a root folder which it scans for content (texture files, XACT sound projects, and spritefonts) in the HarvestContent method. It also has the ability to produce a C# project file that includes information about the harvested content, which it can pass to MSBuild in order to have the corresponding .xnb (et al) files created on your behalf.

To use the VBContentManager class, all you have to do is call the LoadPipeline method in your game's LoadGraphicsContent sub, indicating whether or not the content is already compiled (false) or whether you want to execute the C# hack process (true).

The last thing the LoadPipeline method does for you is it uses the Load method of the underlying ContentManager to import the texture, sound and font files into corresponding typed hash tables, which reside as public members of the VBContentManager. The resources are keyed by filename (no extension) in the respective hash table.

Here is an example of a simple base game class that makes use of the VBContentManager class (you can also watch the video tutorial to see this class being used by a game project).

Public Class MyBaseGame
 Inherits Game

 Protected objGDM As GraphicsDeviceManager
 Protected objCM As VBContentManager
 Protected objSB As SpriteBatch

 ''' <summary>
 ''' Starts a new game for the XNA 2D Engine
 ''' </summary>
 ''' <param name="WindowTitle">The window title when the game is not full-screen</param>
 ''' <param name="MouseIsVisible">Whether or not the mouse is visible by default</param>
 ''' <param name="ContentRootFolder">The root of your content folder, relative to your projects bin\debug (or release) folder</param>
 ''' <remarks></remarks>
 Public Sub New(ByVal WindowTitle As String, ByVal MouseIsVisible As BooleanByVal ContentRootFolder As String)

 Me.objGDM = New GraphicsDeviceManager(Me)
 Me.objCM = New VBContentManager(Me.Services, ContentRootFolder)

 Me.Window.Title = WindowTitle

 Me.IsMouseVisible = MouseIsVisible

 End Sub

 Protected Overrides Sub LoadGraphicsContent(ByVal loadAllContent As Boolean)
 MyBase.LoadGraphicsContent(loadAllContent)

 Me.objCM.LoadPipeline(True)

 End Sub

 Protected Overrides Sub Initialize()
 MyBase.Initialize()

 Me.objSB = New SpriteBatch(Me.objGDM.GraphicsDevice)

 End Sub

End Class



Here is a game that inherits from the above base game class. The root content folder for this game is a peer folder of the project's bin folder. Once you've got all the other stuff sitting in an engine somewhere, this is essentially all you need to do to spawn a new game with a vb content manager, effectively simulating VB support for XNA.

Public Class XNAGameForWindows
 Inherits MyBaseGame

 Public Sub New()
 MyBase.New("XNA Game for Windows"True"../../Content")
 End Sub

End Class


And here's the typed hash table. I wouldn't grab textures & the like out of a hash tables directly from a draw routine, but at the top of your program you can pull them out into discrete references or put them into lists, or whatever. The VBContentManager just makes them available in the hash in a way that's easy for humans to find them (ie. by file name), and you can do whatever you want with them from there.

Public Class Hashtable(Of Type)
 Inherits DictionaryBase

 Public ReadOnly Property Keys() As ICollection
 Get
 Return MyBase.Dictionary.Keys
 End Get
 End Property

 Default Public Property Item(ByVal key As String) As Type
 Get
 Return CType(MyBase.Dictionary(key), Type)
 End Get
 Set(ByVal value As Type)
 MyBase.Dictionary(key) = value
 End Set
 End Property

 Public Overloads Sub Add(ByVal key As String, ByVal obj As Type)
 MyBase.Dictionary.Add(key, obj)
 End Sub

 Public ReadOnly Property Values() As ICollection
 Get
 Return Dictionary.Values
 End Get
 End Property

 Public Function Contains(ByVal key As String) As Boolean
 Return Dictionary.Contains(key)
 End Function

 Public Sub Remove(ByVal key As String)
 Dictionary.Remove(key)
 End Sub

End Class

0 comments: