XNA Framework GameEngine Development. (Part 4, ShaderManager:GameComponent)
Introduction
Welcome to Part 4 of the XNA Framework GameEngine Development series. In this article, I will introduce the shader interface, ShaderManager and the custom effect code generator. This article is the first part of a sub series that will deal with shaders, textures and materials. I know many readers are itching for eye candy by now, it is coming, but for now I am focusing on good base classes and design.
First of many interfaces.
Just a reminder this series of articles is not about using C#, design practices or architecture. That being said, as an Engine developer we need to focus on keeping common interfaces and design principles. This first interface will be used to group effects together into a common parent class. We want to make certain every effect that is used in the engine have common features and are stored in a common way.
Since I use interfaces almost exclusively to accomplish this goal. I added the folder Interfaces to the project and added a class called IRoeShader to the engine project.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Interfaces
{
public interface IRoeShader
{
Effect BaseEffect
{
get;
}
bool ReadyToRender
{
get;
}
void Initialize(GraphicsDevice graphicsDevice);
void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions);
void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool);
}
}
Simple enough, every shader needs to have an effect, indicate if it is ready to be rendered and be initialized. So every class that inherits from IRoeShader will need to implement everything you see here. This will guaruntee the engine can identify a shader effect from every other object in the engine, a nice thing in a huge and complex application.
Building an Effect Proxy Generator.
It might just be the WCF developer in me, but I simply love proxy classes. There is just something wonderfully elegant about using code generators to create useful and powerful object classes. That is why I loved this CodePlex project. With a few simple adjustments we can adjust the code to include our IRoeShader interface.
Note: Many developers with experience using XNA may want to use the generic Content Pipeline for their effects. In a way we are using it here, just I would much rather have code that looks like effect.World = CameraManager.World, than effect.Parameter["World"].SetValue(CameraManager.World). To me it is much more natural to use Properties than Parameter setters.
Create a new console project, I called mine ShaderEffectGenerator.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework.Graphics;
using System.Windows.Forms;
using System.Text;
namespace ShaderEffectCodeGenerator
{
internal class Program
{
// Fields
private static List<string> effectParameters = new List<string>();
private static StreamWriter streamWriter;
private static int tabLevel = 0;
// Methods
private static int LoadEffectFromFile(string fullPath, out Effect effect, out byte[] windowsByteCode, out byte[] xboxByteCode)
{
try
{
Form form = new Form();
PresentationParameters parameters = new PresentationParameters();
GraphicsDevice device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, form.Handle, parameters);
CompiledEffect effect2 = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Windows);
windowsByteCode = effect2.GetEffectCode();
effect = new Effect(device, effect2.GetEffectCode(), CompilerOptions.None, null);
xboxByteCode = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Xbox360).GetEffectCode();
}
catch (Exception exception)
{
ReportError(exception.ToString());
effect = null;
windowsByteCode = null;
xboxByteCode = null;
return -1;
}
return 0;
}
private static void Main(string[] args)
{
if (args.Length < 1)
{
WriteInstructions();
ReportError("Please drag an .fx file into the app.");
}
else
{
Effect effect;
byte[] buffer;
byte[] buffer2;
string path = args[0];
if (path.StartsWith("\"") && path.EndsWith("\""))
{
path = path.Substring(0, path.Length - 1).Substring(1);
}
path = Path.GetFullPath(path);
string str2 = Path.GetDirectoryName(path) + "/" + Path.GetFileNameWithoutExtension(path) + "Effect.cs";
if (LoadEffectFromFile(path, out effect, out buffer, out buffer2) >= 0)
{
if (File.Exists(str2))
{
File.Delete(str2);
}
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
FileStream stream = new FileStream(str2, FileMode.Create);
using (StreamWriter writer = new StreamWriter(stream))
{
streamWriter = writer;
WriteUsingStatements(writer);
WriteLine("namespace RoeEngine2.Shaders");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("public class {0}Effect : IRoeShader", fileNameWithoutExtension));
WriteLine("{");
tabLevel++;
WriteTechniqueEnumeration(effect);
streamWriter.WriteLine();
WriteRoeSpecificProperties(writer);
WriteLine("#region Effect Parameters");
writer.WriteLine();
foreach (EffectParameter parameter in effect.Parameters)
{
WriteEffectParameter(parameter);
writer.WriteLine();
}
WriteLine("#endregion");
writer.WriteLine();
WriteLine("#region Effect Techniques");
writer.WriteLine();
foreach (EffectTechnique technique in effect.Techniques)
{
WriteLine(string.Format("private EffectTechnique _{0}Technique;", technique.Name));
writer.WriteLine();
}
WriteLine("#endregion");
writer.WriteLine();
WriteLine("#region Initialize Methods");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice)");
WriteLine("{");
tabLevel++;
WriteLine("Initialize(graphicsDevice, CompilerOptions.None, null);");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)");
WriteLine("{");
tabLevel++;
WriteLine("Initialize(graphicsDevice, compilerOptions, null);");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
WriteLine("///<param name=\"effectPools\">The EffectPool to use with the effect.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)");
WriteLine("{");
tabLevel++;
WriteLine("_baseEffect = new Effect(graphicsDevice, byteCode, compilerOptions, effectPool);");
WriteLine("_readyToRender = true;");
writer.WriteLine();
foreach (string str4 in effectParameters)
{
WriteLine(string.Format("_{0}Param = _baseEffect.Parameters[\"{0}\"];", str4));
}
writer.WriteLine();
foreach (EffectTechnique technique2 in effect.Techniques)
{
WriteLine(string.Format("_{0}Technique = _baseEffect.Techniques[\"{0}\"];", technique2.Name));
}
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("#endregion");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Sets the current technique for the effect.");
WriteLine("///</summary>");
WriteLine("///<param name=\"technique\">The technique to use for the current technique.</param>");
WriteLine(string.Format("public void SetCurrentTechnique({0}Effect.Techniques technique)", fileNameWithoutExtension));
WriteLine("{");
tabLevel++;
WriteLine("switch (technique)");
WriteLine("{");
tabLevel++;
foreach (EffectTechnique technique3 in effect.Techniques)
{
WriteLine(string.Format("case {0}Effect.Techniques.{1}:", fileNameWithoutExtension, technique3.Name));
tabLevel++;
WriteLine(string.Format("_baseEffect.CurrentTechnique = _{0}Technique;", technique3.Name));
WriteLine("break;");
writer.WriteLine();
tabLevel--;
}
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("#region Compiled Byte Code");
writer.WriteLine();
WriteLine("#if XBOX");
StringBuilder builder = new StringBuilder();
builder.Append("static readonly byte[] byteCode = { ");
foreach (byte num in buffer2)
{
builder.Append(num.ToString() + ",");
}
builder.Append(" };");
WriteLine(builder.ToString());
WriteLine("#else");
builder = new StringBuilder();
builder.Append("static readonly byte[] byteCode = { ");
foreach (byte num2 in buffer)
{
builder.Append(num2.ToString() + ",");
}
builder.Append(" };");
WriteLine(builder.ToString());
WriteLine("#endif");
writer.WriteLine();
WriteLine("#endregion");
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
}
}
}
}
private static void WriteUsingStatements(StreamWriter writer)
{
WriteLine("using System;");
WriteLine("using Microsoft.Xna.Framework;");
WriteLine("using Microsoft.Xna.Framework.Content;");
WriteLine("using Microsoft.Xna.Framework.Graphics;");
WriteLine("using RoeEngine2.Interfaces;");
writer.WriteLine();
}
private static void WriteRoeSpecificProperties(StreamWriter writer)
{
WriteLine("private Effect _baseEffect;");
WriteLine("///<summary>");
WriteLine("///Gets the underlying Effect.");
WriteLine("///</summary>");
WriteLine("public Effect BaseEffect");
WriteLine("{");
tabLevel++;
WriteLine("get { return _baseEffect; }");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("private bool _readyToRender = false;");
WriteLine("///<summary>");
WriteLine("///Is the shader ready to be rendered.");
WriteLine("///</summary>");
WriteLine("public bool ReadyToRender");
WriteLine("{");
tabLevel++;
WriteLine("get { return _readyToRender; }");
tabLevel--;
WriteLine("}");
writer.WriteLine();
}
private static void WriteEffectParameter(EffectParameter param)
{
string item = param.Name;
string str2 = String.Empty;
string str3 = String.Empty;
string str5 = String.Empty;
switch (param.ParameterType)
{
case EffectParameterType.Bool:
str2 = "bool";
str3 = "Boolean";
break;
case EffectParameterType.Int32:
str2 = "int";
str3 = "Int32";
break;
case EffectParameterType.Single:
if ((param.RowCount > 1) && (param.ColumnCount > 1))
{
str2 = "Matrix";
str3 = "Matrix";
}
else if (param.ColumnCount > 1)
{
str2 = "Vector" + param.ColumnCount.ToString();
str3 = str2;
}
break;
case EffectParameterType.String:
str2 = "string";
str3 = "String";
break;
case EffectParameterType.Texture:
str2 = "Texture2D";
str3 = "Texture2D";
break;
case EffectParameterType.Texture1D:
str2 = "Texture1D";
str3 = "Texture1D";
break;
case EffectParameterType.Texture2D:
str3 = "Texture2D";
str3 = "Texture2D";
break;
case EffectParameterType.Texture3D:
str2 = "Texture3D";
str3 = "Texture3D";
break;
case EffectParameterType.TextureCube:
str2 = "TextureCube";
str3 = "TextureCube";
break;
default:
return;
}
if (param.Elements.Count > 0)
{
str2 = str2 + "[]";
}
effectParameters.Add(item);
string str4 = item.Substring(0, 1).ToUpper() + item.Substring(1);
WriteLine(string.Format("private EffectParameter _{0}Param;", item));
WriteLine(string.Format("public {0} {1}", str2, str4));
WriteLine("{");
tabLevel++;
WriteLine("get");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("if (_{0}Param == null)", item));
tabLevel++;
WriteLine(string.Format("throw new Exception(\"Cannot get value of {0}; {0} EffectParameter is null.\");", str4));
tabLevel--;
if (param.Elements.Count > 0)
{
WriteLine(string.Format("return _{0}Param.GetValue{1}Array({2});", item, str3, param.Elements.Count));
}
else
{
WriteLine(string.Format("return _{0}Param.GetValue{1}();", item, str3));
}
tabLevel--;
WriteLine("}");
WriteLine("set");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("if (_{0}Param == null)", item));
tabLevel++;
WriteLine(string.Format("throw new Exception(\"Cannot set value of {0}; {0} EffectParameter is null.\");", str4));
tabLevel--;
WriteLine(string.Format("_{0}Param.SetValue(value);", item));
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
}
private static void WriteInstructions()
{
Console.WriteLine("The EffectCodeGenerator is a drop-in application. Simply drop in any .fx file to have");
Console.WriteLine("the EffectCodeGenerator produce you a new class written in C# that enables easy use");
Console.WriteLine("with your .fx file.");
}
private static void WriteLine(string line)
{
string str = string.Empty;
for (int i = 0; i < tabLevel; i++)
{
str = str + "\t";
}
streamWriter.WriteLine(str + line);
}
private static void WriteTechniqueEnumeration(Effect effect)
{
WriteLine("public enum Techniques");
WriteLine("{");
tabLevel++;
foreach (EffectTechnique technique in effect.Techniques)
{
WriteLine(string.Format("{0},", technique.Name));
}
tabLevel--;
WriteLine("}");
}
private static void ReportError(string error)
{
Console.WriteLine("Error: {0}", error);
Console.ReadKey(true);
}
}
}
ShaderManager class (Updated to use Dictionary)
Note: Thanks again to KaBaL, Please notice I changed the Manager namespace to Managers. This is because there will be more than one.
This class is our main container for everything that has to do with effects and shaders. It includes some very simple principles that you may have seen from the Ramblings of a Hazy Mind series. There are some minor adjustments, but I am not ashamed to say, the Ramblings series was a major contributer to this design principle. Feel free to read the series, it is an excellent primer for many of the next articles in this series.
Add a new class to the Managers folder, I called mine ShaderManager.
Note: Updated to use Dictionary thanks to leaf and KaBaL.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Shaders;
namespace RoeEngine2.Managers
{
public class ShaderManager : GameComponent
{
private static Dictionary<string, IRoeShader> _shaders = new Dictionary<string, IRoeShader>();
private static bool _initialized = false;
/// <summary>
/// Is the ShaderManagers Initialized, used for test cases and setup of Effects.
/// </summary>
public static bool Initialized
{
get { return _initialized; }
}
/// <summary>
/// Create the shader Managers.
/// </summary>
/// <param name="game"></param>
public ShaderManager(Game game)
: base(game)
{
}
/// <summary>
/// Add a shader of type IRoeShader.
/// </summary>
/// <param name="newShader"></param>
/// <param name="shaderLabel"></param>
public static void AddShader(IRoeShader newShader, string shaderLabel)
{
if (shaderLabel != null && !_shaders.ContainsKey(shaderLabel))
{
_shaders.Add(shaderLabel, newShader);
newShader.Initialize(EngineManager.Device);
}
}
/// <summary>
/// Get a shader of type IRoeShader.
/// </summary>
/// <param name="shaderLabel"></param>
/// <returns></returns>
public static IRoeShader GetShader(string shaderLabel)
{
if (shaderLabel != null && _shaders.ContainsKey(shaderLabel))
{
return _shaders[shaderLabel];
}
return null;
}
/// <summary>
/// Create the shaders.
/// </summary>
public override void Initialize()
{
base.Initialize();
foreach (IRoeShader shader in _shaders.Values)
{
if (!shader.ReadyToRender)
{
shader.Initialize(EngineManager.Device);
}
}
AddShader(new basicEffect(), "BasicEffect");
_initialized = true;
}
}
}
There may be some readers that will ask why not use a generic list instead of a hashtable? I am okay with either design, I chose a HashTable because I liked having instant access to each object rathre than getting an item out of a list by index. If you are the kind of person that wants to explore deeper into engine design you might write test cases for each design. It is very possible, a hashtable is sub optimal for the small number of shaders we will most likely be adding to the project. If you can prove it with numbers to support your findings, I will definitely redesign what you see here and give you credit for your work.
Using the ShaderManager GameComponent
Add the following code to the Properties section of RoeEngine2
private static ShaderManager _shaderManagers = null;
And in the RoeEngine2 Constructor
// Init shader Managers _shaderManagers = new ShaderManager(this); Components.Add(_shaderManagers); //TODO include other inits here!
Encapsulating the BasicEffect
This might not be necessary, but since I would like to use the BasicEffect in some samples I will encapsulate it here. This will most likely grow some in the future, it is here now for test cases.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Shaders
{
public class basicEffect : IRoeShader
{
private BasicEffect _baseEffect;
///<summary>
///Gets the underlying Effect.
///</summary>
public Effect BaseEffect
{
get { return _baseEffect; }
}
private bool _readyToRender = false;
///<summary>
///Is the shader ready to be rendered.
///</summary>
public bool ReadyToRender
{
get { return _readyToRender; }
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
public void Initialize(GraphicsDevice graphicsDevice)
{
Initialize(graphicsDevice, CompilerOptions.None, null);
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)
{
Initialize(graphicsDevice, compilerOptions, null);
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
///<param name="effectPools">The EffectPool to use with the effect.</param>
public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)
{
_baseEffect = new BasicEffect(graphicsDevice, effectPool);
_readyToRender = true;
}
}
}
Optional TDD – Updated, no longer using InvalidOperationException
Add the following test cases to the RoeEngineTestCases class.
[Test, Category("ShaderManager")]
public void ShaderManagerCreated()
{
Assert.IsTrue(ShaderManager.Initialized);
Assert.IsNotNull(ShaderManager.GetShader("BasicEffect"));
Assert.AreEqual(typeof(basicEffect), ShaderManager.GetShader("BasicEffect").GetType());
}
[Test, Category("ShaderManager")]
public void ShaderManagerAddGetFailed()
{
// It is always a good idea to test for Failures too!
Assert.IsNull(ShaderManager.GetShader("testshader"));
}
Conclusion
In this article I introduced the IRoeShader, shaders, shader proxy generation and the ShaderManager. I will be revisting this class from time to time, in a growing engine it is always necessary to Refactor your work. Hopefully, you can use some of these design practices in other projects as well.
Your feedback is very important, please leave me a comments or suggestions. I will respond as soon as possible.