This tutorial we are going to look at SpriteList and show how it can result in a massive increase in performance. Along the way, we are going to create two custom Scene classes, one that performs poorly, and one the does not. Along the way we are going to demonstrate dynamically generating a UI and how UI scenes and GameEngine2D scenes can co-exist.
A lot of the following code we have covered in previous tutorials, so I am not going to go into a great deal of detail.
First lets take a look at our AppMain.cs
using System; using System.Collections.Generic; using Sce.Pss.Core; using Sce.Pss.Core.Environment; using Sce.Pss.Core.Graphics; using Sce.Pss.Core.Input; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; using Sce.Pss.HighLevel.UI; namespace SpriteList { public class AppMain { static private bool quit = false; public static void Main(string[] args) { Director.Initialize(); Director.Instance.RunWithScene(new Sce.Pss.HighLevel.GameEngine2D.Scene(),true); UISystem.Initialize(Director.Instance.GL.Context); Sce.Pss.HighLevel.UI.Panel dialog = new Panel(); dialog.Width = Director.Instance.GL.Context.GetViewport().Width; dialog.Height = Director.Instance.GL.Context.GetViewport().Height; Button buttonUI1 = new Button(); buttonUI1.Name = "buttonGoSlow"; buttonUI1.Text = "Slow version"; buttonUI1.Width = 300; buttonUI1.Height = 50; buttonUI1.Alpha = 0.8f; buttonUI1.TouchEventReceived += (sender, e) => { Director.Instance.ReplaceScene(new SlowVersion()); }; Button buttonUI2 = new Button(); buttonUI2.Name = "buttonGoFase"; buttonUI2.Text = "Fast version"; buttonUI2.Width = 300; buttonUI2.Height = 50; buttonUI2.SetPosition(0,55); buttonUI2.Alpha = 0.8f; buttonUI2.TouchEventReceived += (sender, e) => { Director.Instance.ReplaceScene(new FastVersion()); }; Button buttonUI3 = new Button(); buttonUI3.Name = "buttonExit"; buttonUI3.Text = "Exit App"; buttonUI3.Width = 300; buttonUI3.Height = 50; buttonUI3.SetPosition(0,110); buttonUI3.Alpha = 0.8f; buttonUI3.TouchEventReceived += (sender, e) => { quit = true; }; dialog.AddChildLast(buttonUI1); dialog.AddChildLast(buttonUI2); dialog.AddChildLast(buttonUI3); Sce.Pss.HighLevel.UI.Scene scene = new Sce.Pss.HighLevel.UI.Scene(); scene.RootWidget.AddChildLast(dialog); UISystem.SetScene(scene); while(!quit) { Director.Instance.Update(); Director.Instance.GL.Context.Clear(); Director.Instance.Render(); UISystem.Update(Touch.GetData(0)); UISystem.Render(); Director.Instance.GL.Context.SwapBuffers(); Director.Instance.PostSwap(); } } } }
We set up our Director object and tell it to run with an empty scene. We then set up our UISystem in a similar manner. Then we create a Panel object named dialog by hand. This process is basically the same as what UI Composer generates for us, just all created in code. We then add 3 buttons to our panel. For each button we add a TouchEventReceived handler in the form of a lamda function.
If they click the Slow Version button, we create a new SlowVersion scene object and set it as the active scene in the Director. If we click the Fast Version button, we create a FastVersion scene object and make it the active scene. We will cover creating these two classes in just a moment. Finally for the Exit App button, we toggle the quit bool to true, causing the event loop to exit.
Now that we have created our 3 buttons, we add them to our dialog Panel. We then create an empty Scene ( UI scene, not GameEngine2D! ), add our dialog to it and set it as the active scene. This will render our buttons visible.
Now we handle our event loop. Everything here is pretty much identical to in previous tutorials, the only key difference is that we call our UISystem.Update() and Render() within the game loop. It is important that UISystem.Render is called after Director.Instance.Render, or the Director will render over top of it! This loop will now continue processing until the quit bool is set by pressing the Exit App button.
Now lets take a look at SlowVersion.cs… the bad way of doing things!
using System; using Sce.Pss.Core.Graphics; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SpriteList { public class SlowVersion : Scene { private TextureInfo _textureInfo; private Texture2D _texture2D; private System.Collections.Generic.List<SpriteUV> _sprites; public SlowVersion () { this.Camera.SetViewFromViewport(); _texture2D = new Texture2D("/Application/Jet.png",false); _textureInfo = new TextureInfo(_texture2D); var w = Director.Instance.GL.Context.GetViewport().Width; var h = Director.Instance.GL.Context.GetViewport().Height; System.Random rand = new System.Random(); _sprites = new System.Collections.Generic.List<SpriteUV>(); for(int i = 0; i < 1000; i++) { SpriteUV sprite = new SpriteUV(_textureInfo); sprite.Position = new Sce.Pss.Core.Vector2(rand.Next(0,w) - w/2 ,rand.Next(0,h) -h/2); sprite.Quad.S = new Sce.Pss.Core.Vector2(_texture2D.Width,_texture2D.Height); sprite.Rotate(rand.Next (0,360)); sprite.Schedule( (dt) => { sprite.Position = new Sce.Pss.Core.Vector2(rand.Next(0,w)-w/2 + _texture2D.Width/2,rand.Next(0,h)-h/2); }); _sprites.Add(sprite); } foreach(var sprite in _sprites) { this.AddChild(sprite); } FPS fps = new FPS(); fps.Position = new Sce.Pss.Core.Vector2(0,0); this.AddChild(fps); } public override void Draw () { base.Draw (); } } }
Most everything here has also been covered in prior tutorials, but perhaps not in this form. SlowVersion is inherited from Scene. For member variables it contains a single Texture2D and it’s corresponding TextureInfo object. It also contains a List of SpriteUV’s to be rendered every frame. All of the sprites point to the same Texture, our Jet.png graphic:
Bonus points if you can identify the type of jet!
In our constructor we go about the usual things, first we setup our camera, load our texture from disk and create our TextureInfo from the loaded texture. Next we allocate our List and then create 1000 instances of our jet sprite, randomly rotated and positioned on screen. We also register a lamda Schedule method that will be called every frame. During each update we simply randomly relocate the airplane sprite on screen. Finally, we add the newly created sprite to our list. Next we loop through all of our sprites and add them to our scene.
In the next bit we create an FPS object, a simple helper object for displaying the current frame rate on screen. We will cover the code in a second.
Now let’s take a look at FastVersion.cs, which you will soon notice is basically just a copy and paste job!
using System; using Sce.Pss.Core.Graphics; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SpriteList { public class FastVersion : Scene { private TextureInfo _textureInfo; private Texture2D _texture2D; private System.Collections.Generic.List<SpriteUV> _sprites; private Sce.Pss.HighLevel.GameEngine2D.SpriteList _spriteList; public FastVersion () { this.Camera.SetViewFromViewport(); _texture2D = new Texture2D("/Application/Jet.png",false); _textureInfo = new TextureInfo(_texture2D); var w = Director.Instance.GL.Context.GetViewport().Width; var h = Director.Instance.GL.Context.GetViewport().Height; System.Random rand = new System.Random(); _sprites = new System.Collections.Generic.List<SpriteUV>(); for(int i = 0; i < 1000; i++) { SpriteUV sprite = new SpriteUV(_textureInfo); sprite.Position = new Sce.Pss.Core.Vector2(rand.Next(0,w) - w/2 ,rand.Next(0,h) -h/2); sprite.Quad.S = new Sce.Pss.Core.Vector2(_texture2D.Width,_texture2D.Height); sprite.Rotate(rand.Next (0,360)); sprite.Schedule( (dt) => { sprite.Position = new Sce.Pss.Core.Vector2(rand.Next(0,w)-w/2,rand.Next(0,h)-h/2); }); _sprites.Add(sprite); } _spriteList = new Sce.Pss.HighLevel.GameEngine2D.SpriteList(_textureInfo); foreach(var sprite in _sprites) { _spriteList.AddChild(sprite); } FPS fps = new FPS(); fps.Position = new Sce.Pss.Core.Vector2(0,0); this.AddChild (_spriteList); this.AddChild(fps); } public override void Draw () { base.Draw (); } } }
You may notice two things… first, neither SlowVersion nor FastVersion do *ANY* cleanup and leak like sieves! In non-demonstration code, be sure to clean up after yourself!
Second, they are virtually identical, but if you run them, FastVersion runs easily 4-5x faster… why is this?
That is the power of SpriteList, which performs a function very similar to SpriteBatch if you are familiar with XNA programming. We only made two changes.
1:
private Sce.Pss.HighLevel.GameEngine2D.SpriteList _spriteList;
We declared out spriteList member.
2:
_spriteList = new Sce.Pss.HighLevel.GameEngine2D.SpriteList(_textureInfo); foreach(var sprite in _sprites) { _spriteList.AddChild(sprite); }
Then, instead of adding the sprites to the scene, we add them to our spriteList object. That’s it!
There are limitations, all sprites must have a common TextureInfo, BlendMode and Color property to be added to a spriteList, so basically create one spriteList per TextureInfo if you have multiple sprites on screen and you will see a large performance boost.
Finally lets take a quick look at the FPS.cs widget. This is just a crude hack to display FPS on screen by creating a small texture.
using System; using Sce.Pss.Core; using Sce.Pss.Core.Graphics; using Sce.Pss.Core.Imaging; using Sce.Pss.HighLevel.GameEngine2D; using Sce.Pss.HighLevel.GameEngine2D.Base; namespace SpriteList { public class FPS : SpriteUV { TextureInfo _ti; public FPS () { Texture2D texture = new Texture2D(150,1000,false, PixelFormat.Rgba); _ti = new TextureInfo(texture); this.TextureInfo = _ti; this.Quad.S = new Sce.Pss.Core.Vector2(150,100); Scheduler.Instance.ScheduleUpdateForTarget(this,1,false); } public override void Update (float dt) { _ti.Dispose(); Image img = new Image(ImageMode.Rgba, new ImageSize(150,100), new ImageColor(255,255,255,0)); img.DrawText("FPS:" + (1/dt).ToString(), new ImageColor(255,255,255,255), new Font(FontAlias.System,32,FontStyle.Bold), new ImagePosition(0,0)); Texture2D texture = new Texture2D(150,100,false, PixelFormat.Rgba); texture.SetPixels(0,img.ToBuffer(),PixelFormat.Rgba); img.Dispose(); _ti = new TextureInfo(texture); this.TextureInfo = _ti; base.Update (dt); } } }
All we are doing here is creating our own SpriteUV derived object, FPS, but instead of loading the texture from file, we generate an image dynamically, just like we did way back in Hello World. We then schedule ourselves to update every frame. In the update we create an image and print the the current frames per second ( the fraction of a second our current time delta takes) and update our texture to the newly created image.
Here is our code in action. Unfortunately the nature of the output resulted in a horrifically large animated gif, so I had to put this one up on YouTube:
You can download all of the project code here.