Disecting ShipHo! - Scene managementBy Rim van Wersch, January 16 2006 |
Now that we've got our resource management taken care of, we'll move on to getting something on the screen. So what do we need for that? As we've seen in the previous section, a class for representing scene objects will allow us to easily render the meshes we load with the resource manager, so we'll use that as our starting point. In addition to this, we'll also want to be able to set the position, scale and orientation of these scene objects. And finally we need information on hit regions, renderstates and various other useful properties of the scene object. This is where our ShipHo.Common.Entity class comes in, which we'll explore in a bit.
Before we start delving into the implementation of the Entity class, we should consider how we're going to manage all these enitities that make up a scene. Obviously a scene class comes to mind, so what should that contain? Since we won't be rendering massive amounts of meshes, we don't need to look into spatial scene organization, like quadtrees or octrees, and we can get away with using a simple ArrayList to store our entities.
Besides storing a list of entities, we'll want to update and actually render the scene too. We'll also add a Reset(Device device) and Dispose() method to our scene, for handling resources created by the game itself and as such are not handled by our ResourceManager, like the mesh for our water plane. And finally we'll need a Prepare(Device device) method to take care of things that need to be done after the update and just before the rendering of the scene, like rendering our water reflection.
Introducing the ShipHo.Common.Entity class
We've got a whole lot to look at on ShipHo!'s scene management, so let's get started. We've already talked about the Entity class that encapsulates objects in a scene and how it supports a number of properties to determine how this object should be rendered. The following code snippet from this class shows how these properties are used in the entity's Render(Device device) method.
protected void Initialize() { ? ?this.meshDescription = ResourceManager.DefaultManager.GetMeshDescription( meshName ); ? ?this.initialized = true; ? ?this.alphaBlending = meshDescription.hasAlphaTextures; } public virtual Matrix WorldTransform { ? ?get ? ?{ ? ? ? ?return Matrix.Scaling(Scale.X, Scale.Y, scale.Z) * this.RotationMatrix * Matrix.Translation( Position ); ? ?} } public virtual void Render(Device device, Cull cullMode) { ? ?if (!initialized) ? ?{ ? ? ? ?Initialize(); ? ?} ? ?if (!this.shouldBeRendered) ? ?{ ? ? ? ? ? ? ? ? ? ? ? ?return; ? ?} ? ?device.RenderState.FogEnable = this.FogEnabled; ? ?device.RenderState.Lighting = this.Lighting; ? ?Mesh mesh = ResourceManager.DefaultManager.GetMesh( meshDescription.meshName ); ? ?device.Transform.World = this.WorldTransform; ? ?if (this.AlphaBlending) ? ?{ ? ? ? ?// use cullmode sorting trick ? ? ? ? ? ? ? ? ? ? ? ?device.RenderState.CullMode = (cullMode == Cull.CounterClockwise ? Cull.Clockwise : Cull.CounterClockwise); ? ? ? ?device.RenderState.ZBufferWriteEnable = false; ? ? ? ?device.RenderState.AlphaBlendEnable = true; ? ? ? ?for(int i = 0; i < meshDescription.subsets; i++) ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.Material = meshDescription.meshMaterials[i]; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.SetTexture( 0, ResourceManager.DefaultManager.GetTexture( meshDescription.textureNames[i] ) ); ? ? ? ? ? ?mesh.DrawSubset(i); ? ? ? ?} ? ? ? ?device.RenderState.CullMode = cullMode; ? ? ? ?device.RenderState.ZBufferWriteEnable = true; ? ? ? ?for(int i = 0; i < meshDescription.subsets; i++) ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.Material = meshDescription.meshMaterials[i]; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.SetTexture( 0, ResourceManager.DefaultManager.GetTexture( meshDescription.textureNames[i] ) ); ? ? ? ? ? ?mesh.DrawSubset(i); ? ? ? ?} ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.RenderState.AlphaBlendEnable = false; ? ?} ? ?else ? ?{ ? ? ? ?device.RenderState.CullMode = cullMode; ? ? ? ?for(int i = 0; i < meshDescription.subsets; i++) ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.Material = meshDescription.meshMaterials[i]; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.SetTexture( 0, ResourceManager.DefaultManager.GetTexture( meshDescription.textureNames[i] ) ); ? ? ? ? ? ? ? ? ? ? ? ?mesh.DrawSubset(i); ? ? ? ?} ? ?} ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?device.RenderState.FogEnable = true; ? ?device.RenderState.Lighting = true; ? ? }
Don't be daunted by the length of the method, since it doesn't differ that much from the little snippet shown in our example on Resource Management. Let's go through it one step at a time. First we'll check if we need to initialize the entity and do so if necessary, by fetching the MeshDescription from the ResourceManager. Then we check if this entity should be rendererd at all and bail out if it shouldn't. This is used for applying view frustrum culling in ShipHo!, so it's set in the update method of the Scene object we'll see later.
Once we're set to render the object and are sure that it should be rendered, we set the appropriate renderstates on the device, fetch the mesh object from our resource manager and set the world transformation matrix on the device, which we obtain by combining our position translation, orientation rotation and any scaling operation into a single matrix.
Then we have two render paths for the entities, one for entities that use alpha textures and one for those that don't. If you scroll down the code block displayed above, you'll see our familiar simple rendering method for rendering entities that don't have alpha textures. We just set the appropriate materials and textures for each subset and render these subsets like normal.
For meshes that do use alpha textures, we need to render the subsets in a back-to-front order. This is necessary to make sure any parts of the mesh that are behind a part with alpha transparancy are rendered first, so the parts in the back will 'show through' on (partially) transparent parts. There are a number of ways to do this and we've opted for the cullmode sorting trick as used in CodeSampler's sample, which is explained in more detail in this code snippet. It's not the most efficient way to do this and it has a number of limitations, but it'll do for our simple models.
So with the rendering we've covered the main functionality of the Entity class. It contains some more information on the mesh used to render the entity, like a hit region and information describing its bounding sphere. We'll look into using these properties in part 6 of this series, Collision detection. You may have also noticed the IUpdateable interface defined at the top of the Entity.cs file. Any entities that have some updating to do should implement this interface, which will tell our Scene object to automatically update them. We'll see how that works later on in this part of the series and we'll look into the typical implementation of this method in more detail in part 5, Moving objects.
A look at the ShipHo.Common.Scene class
As we've set out in our requirements, the ShipHo.Common.Scene will hold our ShipHo.Common.Entity objects, so we can easily add, update, render and remove them. First we'll take a look at how these entities are stored in our scene class. Since we'll be using an ArrayList to store the objects internally, a lot of work is already done for us, but there still is a little pitfall we need to code around. Let's take a look at the code for adding and removing entities first, in which this issue is readily apparent.
public enum EntityListAction { ? ?Add, ? ?Remove } public struct EntityListAlteration { ? ?public EntityListAction Action; ? ?public Entity Entity; } public class Scene : IUpdateable { ? ?private ArrayList entityList = new ArrayList(); ? ?public ArrayList Entities ? ?{ ? ? ? ?get { return entityList; } ? ?} ? ?private ArrayList entityListAlterations = new ArrayList(); ? ?//...more Scene code here ? ?public void AddEntity( Entity entity ) ? ?{ ? ? ? ?/* ? ? ? ?entityList.Add( entity ); ? ? ? ? ? ? ? ? ? ?entity.CurrentScene = this; ? ? ? ?*/ ? ? ? ?EntityListAlteration alteration; ? ? ? ?alteration.Action = EntityListAction.Add; ? ? ? ?alteration.Entity = entity; ? ? ? ?entityListAlterations.Add(alteration); ? ?} ? ?public void RemoveEntity( Entity entity ) ? ?{ ? ? ? ?/* ? ? ? ?entityList.Remove( entity ); ? ? ? ?entity.CurrentScene = null; ? ? ? ?*/ ? ? ? ?EntityListAlteration alteration; ? ? ? ?alteration.Action = EntityListAction.Remove; ? ? ? ?alteration.Entity = entity; ? ? ? ?entityListAlterations.Add(alteration); ? ?} ? ?//...more Scene code here ? ?// called as the first thing in the scene update method ? ?private void HandleEntityListAlterations() ? ?{ ? ? ? ?for (int i = 0; i < entityListAlterations.Count; i++) ? ? ? ?{ ? ? ? ? ? ?EntityListAlteration alteration = (EntityListAlteration)entityListAlterations[i]; ? ? ? ? ? ?switch( alteration.Action ) ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?case EntityListAction.Add: ? ? ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ? ?entityList.Add( alteration.Entity ); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?alteration.Entity.CurrentScene = this; ? ? ? ? ? ? ? ? ? ?break; ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ?case EntityListAction.Remove: ? ? ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ? ?entityList.Remove( alteration.Entity ); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?alteration.Entity.CurrentScene = null; ? ? ? ? ? ? ? ? ? ?break; ? ? ? ? ? ? ? ?} ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?entityListAlterations.Clear(); ? ?} ? ?//...more Scene code here }
Now this code probably looks a bit more complex that you might expect. After all, you could just have the AddEntity and RemoveEntity methods manipulate the entitiyList arraylist directly, right? I thought so too (as you can see from those lines that have been commented out), but this turns out to give us a bit of a problem when updating the entities.
An entitiy may produce a new entity in its Update method (launching a projectile for example) or it may remove itself or another object from the scene (for example, when a projectile hits). If we directly manipulate the entitiyList arraylist for those events, that would cause the entitiyList to be modified while we're enumerating through the objects stored in it, which will throw a nice InvalidOperationException. Go ahead and uncomment the lines in the AddEntity and RemoveEntity methods and fire a torpedo ingame to see this in action, if you must
Of course there are several ways to code around this problem and the solution presented may not be the best, but I think it elegantly deals with our problem. When we need to modify the entitiyList, we store an EntityListAlteration object in our entityListAlterations (which is also just an arraylist). In the beginning of the next frame, the first thing the Update method will do, is check the entityListAlterations arraylist and perform any pending alterations. This takes care of our little problem and we can proceed with the rest of the update, for which you'll find the code below.
// defined in Entity.cs (where it should be, according to some class cohesion guidelines, I _think_) public struct SceneInformation { ? ?public GameCamera Camera; ? ?public float FrameTime; ? ?public double ApplicationTime; ? ?public SceneInformation( GameCamera camera, float elapsedTime, double appTime ) ? ?{ ? ? ? ?this.Camera = camera; ? ? ? ?this.FrameTime = elapsedTime; ? ? ? ?this.ApplicationTime = appTime; ? ?} } // defined in our Scene class public void Update(SceneInformation info) { ? ?lastInfo = info; ? ?HandleEntityListAlterations(); ? ?// fixed updates ? ?waterPlane.Update( info ); ? ? ? ? ? ? ? ?skyEntity.Position = new Vector3( info.Camera.Target.X, 0, info.Camera.Target.Z ); ? ?waterPlane.Position = new Vector3( info.Camera.Target.X, 0, info.Camera.Target.Z ); ? ?birdController.Center = new Vector3( info.Camera.Target.X, 110, info.Camera.Target.Z ); ? ? ? ?foreach(Entity entity in entityList) ? ?{ ? ? ? ?IUpdateable updateable = entity as IUpdateable; ? ? ? ?if (updateable != null) ? ? ? ?{ ? ? ? ? ? ?updateable.Update(info); ? ? ? ?} ? ? ? ?entity.ShouldBeRendered = info.Camera.EntityInFrustum( entity ); ? ?} ? ?ParticleEngine.DefaultEngine.Update( info ); }
With the HandleEntityListAlterations() method taken care of, the rest of the update code is relatively simply. Let's go through it step by step. We get the SceneInformation provided from the main application, which contains all kinds of useful information on the scene. First, we'll store it to have it around whenever we need it outside of the Update method. Next we call our HandleEntityListAlterations() as explained above and then we move on to the code to actually update our scene and the entities it contains.
We'll begin with our 'fixed updates', where we basically do all our updating that does not involve our typical entities. As you can see, we'll update our water plane and move the skybox, the water plane and our birds's 'point of interest' to the current camera position. This is a cheap way of making our world seem infinite and though it doesn't make for a superbly immersive game, it works well enough for our simple little shooter.
After our fixed updates, we enumerate through our list of entities and update them if they implement the IUpdateable interface. Most updateable entities will be instances of the MobileEntity class, so we'll see how this works out in part 5 of this series, Moving objects. For now, we'll continue with our view frustrum culling. Our GameCamera class (which is explored more fully in part 4, A game camera) has a convenient method to check if an entity lies within the current view frustum. So we'll just set the ShouldBeRendered property accordingly on our entities to make sure they're only rendered when they are visible to the camera.
And finally we update our particle system. We update that one last, because our entities may create new particles in their update methods (when a torpedo hits or a ship sinks) and we want these new particles to be properly initialized before we move on to the rendering phase. We'll take a closer look at our ParticleEngine implementation in part 7 of this series, aptly named A particle system.
Finally! Rendering a scene
The rest of the methods of the Scene class aren't very interesting. The Reset(Device device) and Dispose() methods only have to take care of our water plane, since all the other entities are already handled by our resource manager. There are only some utility methods left for easily adding ships and various other things which aren't doing anything fancy either, so let's move on to our rendering code below.
public void Prepare( Device device ) { ? ?// preparation ? ?try ? ?{ ? ? ? ?waterPlane.BlitSeaLayers( device ); ? ? ? ? ? ? ? ?waterPlane.RenderReflection( device, this ); ? ? ? ? ? ? ? ? ? ? ? ? ? ?} ? ?catch(Exception e) ? ?{ ? ? ? ?System.Diagnostics.Debug.WriteLine( e ); ? ?} } public void Render( Device device ) { ? ?// main pass ? ?RenderOptions options = new RenderOptions( RenderPass.Normal ); ? ?RenderScene( device, options ); ? ?ParticleEngine.DefaultEngine.Render( device ); } internal void RenderScene( Device device, RenderOptions options) { ? ?Cull cullMode = (options.RenderPass == RenderPass.Reflection ? Cull.Clockwise : Cull.CounterClockwise); ? ?// render opague entities, save alpha blending ones ? ?ArrayList alphaBlenders = new ArrayList(); ? ?foreach( Entity entity in entityList) ? ?{ ? ? ? ?if (entity.AlphaBlending) ? ? ? ?{ ? ? ? ? ? ?// render later ? ? ? ? ? ?if (entity is Wake) ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?// always render wakes last ? ? ? ? ? ? ? ?alphaBlenders.Add(entity); ? ? ? ? ? ?} ? ? ? ? ? ?else ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?alphaBlenders.Insert(0, entity); ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?else ? ? ? ?{ ? ? ? ? ? ?entity.Render( device, cullMode ); ? ? ? ?} ? ?} ? ?// render fixed entities ? ?if (options.RenderPass != RenderPass.Reflection) ? ?{ ? ? ? ?waterPlane.Render( device ); ? ?} ? ? ? ?// render alpha blending entities ? ?foreach( Entity entity in alphaBlenders ) ? ?{ ? ? ? ?entity.Render( device, cullMode ); ? ?} }
Now, where to start? The first interesting thing about our rendering code would be our Prepare method. This method is called from our application's main render method, but before the actual scene is rendered (so before we call device.BeginScene()). This allows us to use the RenderToTexture helper class (i.e. switch the device's render target) to perform any preparing we need to do without any problems.
In ShipHo, we use this to perform a RenderToTexture pass to render our scene into the reflective sea texture. You can read more about this technique for reflection rendering in this tutorial, but for now it's enough to know that this technique requires the scene to be rendered twice. By defining our RenderScene method (where the actual rendering is done) as internal, we can call it from our waterPlane object to render the reflection (in the waterPlane.RenderReflection method). Our waterPlane.BlitSeaLayers method just blends two 'scrolling' wave textures onto the sea texture to give the illusion of actual waves. Again not the most advanced technique, but it works out quite nicely for ShipHo.
Our Render method also just calls the RenderScene method, so let's see what goes on there. First we set up the correct cullmode, which depends on whether we're rendering the scene's reflection or the actual scene (this has to do with the inverted view matrix). Then we'll define an arraylist to hold any objects that use alpha blending. This will allow us to render these last, after all opaque objects have been rendered. Next we enumerate through our entities again, rendering the opaque ones and storing the transparent ones for later (the wakes should be rendered last because they will overlap the reflection of any other transparent objects on the water).
After we've rendered our opaque objects, we'll render our waterPlane object. We need to render this one manually so we can hide it when we're rendering our reflection. And finally we render our transparent objects, which will now nicely blend onto the scene now without much trouble. Note that there's one thing missing here, namely that the transparent objects are rendered in an arbitrary order (aside from the wakes). This isn't much of a problem in ShipHo since we don't have too many transparent object to worry about, but it would be better to sort these transparent objects in a back-to-front order based on their distance to the camera. It's interesting to note that our wakes still should be rendered last though.
Wrapping it up
This conludes our section on the scene management of ShipHo! Hopefully this article clarified how it works and maybe gave you some ideas on how to go about scene management yourself. In the next part of this series, we'll be taking a closer look at ShipHo's game camera, tracking objects and view frustum culling.
Back to Resource Management | Continue reading about the Game Camera
Files for this article
Filename | Size |
? ShipHo source project.zip | 4.2 MB |