Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Microsoft XNA 4.0 Game Development Cookbook

You're reading from   Microsoft XNA 4.0 Game Development Cookbook This book goes further than the basic manuals to help you exploit Microsoft XNA to create fantastic virtual worlds and effects in your 2D or 3D games. Includes 35 essential recipes for game developers.

Arrow left icon
Product type Paperback
Published in Jun 2012
Publisher Packt
ISBN-13 9781849691987
Length 356 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Luke Drumm Luke Drumm
Author Profile Icon Luke Drumm
Luke Drumm
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Microsoft XNA 4.0 Game Development Cookbook
Credits
About the Author
About the Reviewers
www.PacktPub.com
1. Preface
1. Applying Special Effects FREE CHAPTER 2. Building 2D and 3D Terrain 3. Procedural Modeling 4. Creating Water and Sky 5. Non-Player Characters 6. Playing with Animation 7. Creating Vehicles 8. Receiving Player Input 9. Networking

Implementing smoke within the Reach profile


If implemented in a naive fashion, rendering smoke could place a significant burden on the hardware of a device running a game under the Reach profile.

In this recipe, you'll learn a method for improving the distribution of work and data between the CPU and GPU, to hopefully get as close as possible to the unachievable goal of hardware instancing in an environment that doesn't allow for custom shaders.

In the following illustration, you can see this recipe in use within a stylized city scene:

Getting ready

An image of a smoke "particle" is required for this special effect, but don't feel pressured to spend too much time or effort creating anything too elaborate. Although it forms the heart of the display, the technique's repetition and distortion of even the most basic smoke pattern can result in some quite intricate and realistic results.

How to do it...

To create smoke within the Reach Profile:

  1. 1. Start by creating a new smoke particle class:

    class ReachSmokeParticle
    {
    
  2. 2. Insert instance variables for the position and age of the particle:

    public Vector3 Position;
    public float PositionDelta;
    public float Scale;
    public float Rotation;
    public float RotationDelta;
    public float Age;
    public float AgeDelta;
    public bool Visible;
    public Texture2D Texture;
    public Vector2 TextureOrigin;
    
  3. 3. Add an Update() method to calculate a particle's details:

    public void Update(
    GameTime gameTime,
    Vector3 wind,
    Vector3 spawnPoint,
    float spawnRadius,
    Random random)
    {
    var timeScale = (float)gameTime.ElapsedGameTime.TotalSeconds;
    Position += ((Vector3.Up * PositionDelta) + wind) *
    timeScale;
    Rotation += RotationDelta * timeScale;
    Age += AgeDelta * timeScale;
    if (Age > 1)
    {
    var offset = ((.5f - (float)random.NextDouble()) *
    (Vector3.Right * spawnRadius)) +
    ((.5f - (float)random.NextDouble()) *
    (Vector3.Forward * spawnRadius));
    Position = spawnPoint + offset;
    Age = 0;
    Visible = true;
    }
    }
    
  4. 4. Continue by adding a Draw() method:

    public void Draw(
    SpriteBatch spriteBatch,
    Viewport viewport,
    Matrix view,
    Matrix projection,
    float projectedScale)
    {
    if (!Visible)
    {
    return;
    }
    var projectedPosition = viewport.Project(
    Position,
    projection,
    view,
    Matrix.Identity);
    var screenPosition = new Vector2(
    projectedPosition.X, projectedPosition.Y);
    var tint = Color.FromNonPremultiplied(
    255, 255, 255, 255 - (int)(255f * Age));
    var displayScale = Scale * projectedScale;
    spriteBatch.Draw(
    Texture, screenPosition,
    null, tint, Rotation, TextureOrigin,
    displayScale,
    SpriteEffects.None, projectedPosition.Z);
    }
    
  5. 5. Now, add the class that will be emitting the freshly defined particles:

    class ReachSmoke
    {
    
  6. 6. Insert some instance variables to the new class to hold the details of the particles:

    SpriteBatch spriteBatch;
    Texture2D smoke;
    Vector2 halfSmokeSize;
    List<ReachSmokeParticle> particles;
    Vector3 spawnPoint = Vector3.Zero;
    float spawnRadius = 0.2f;
    Random random = new Random();
    Vector3 wind = Vector3.Right * 0.3f;
    
  7. 7. Add a constructor to create instances of all the particles:

    public ReachSmoke(
    GraphicsDevice graphicsDevice,
    ContentManager content)
    {
    spriteBatch = new SpriteBatch(graphicsDevice);
    smoke = content.Load<Texture2D>("smoke/smoke");
    halfSmokeSize = new Vector2(
    smoke.Width / 2, smoke.Height / 2);
    var particleCount = 300;
    particles = new List<ReachSmokeParticle>();
    for (var index = 0; index < particleCount; index++)
    {
    var particle = new ReachSmokeParticle()
    {
    Texture = smoke,
    TextureOrigin = halfSmokeSize,
    Position = spawnPoint,
    PositionDelta =
    (0.8f * (float)random.NextDouble()) + .2f,
    Scale =
    (0.8f * (float)random.NextDouble()) + .2f,
    Rotation = (float)random.NextDouble(),
    RotationDelta = 0.5f - (float)random.NextDouble(),
    Age = (float)random.NextDouble(),
    AgeDelta =
    (0.8f * (float)random.NextDouble()) + .2f,
    Visible = false
    };
    particles.Add(particle);
    }
    }
    
  8. 8. Add an Update() method to update all the particles to their latest positions:

    public void Update(GameTime gameTime)
    {
    foreach (var particle in particles)
    {
    particle.Update(
    gameTime,
    wind,
    spawnPoint, spawnRadius,
    random);
    }
    }
    
  9. 9. Finish the class with the addition of the Draw() method to render the particles onto the screen:

    public void Draw(
    Matrix view, Matrix projection, Matrix world,
    Viewport viewport)
    {
    var scaleTestPositionOne = viewport.Project(
    Vector3.Zero,
    projection, view, Matrix.Identity);
    if (scaleTestPositionOne.Z < 0)
    {
    return;
    }
    var scaleTestPositionTwo = viewport.Project(
    Vector3.Up + Vector3.Right,
    projection, view, Matrix.Identity);
    var projectedScale = Vector3.Distance(
    scaleTestPositionOne, scaleTestPositionTwo) /
    (smoke.Height * 2);
    if (projectedScale > 5f)
    {
    return;
    }
    spriteBatch.Begin(
    SpriteSortMode.Deferred,
    BlendState.AlphaBlend, null,
    DepthStencilState.DepthRead, null);
    foreach (var particle in particles)
    {
    particle.Draw(spriteBatch,
    viewport, view, projection, projectedScale);
    }
    spriteBatch.End();
    }
    

How it works...

Inspecting the constructor of the ReachSmoke class, we can see the creation of the smoke particles.

A diverse selection of random sizes, speeds, and states throughout the particles lessens the chance of players being able to spot any obvious signs of repetition, despite the use of only one texture across all of the particles.

In order to lessen the chance of unwanted pressure on the garbage collector, we create a set number of particles at the beginning and recycle them as required.

Within the Update() method of the ReachSmokeParticle class, the code to move and rotate each particle can be seen along the recycling process that re-spawns a particle once it has reached the end of its life.

Eagle-eyed readers may notice that a given particle's visibility isn't enabled until it has re-spawned at least once. This delay in visibility is done to give the particles the best chance of appearing in a reasonably even manner, and avoid one large, unrealistic clump at the beginning.

To enjoy the best chance of maximum performance, the Draw() methods of both the ReachSmoke and ReachSmokeParticle classes are designed with the idea of harnessing the optimizations present in the SpriteBatch class.

With this in mind, the transformation from 3D world space into 2D screen space is performed on the CPU via .NET code. This allows the GPU to receive and display the particles in one unified update, without the need to recalculate or reload.

Handling the 3D calculations ourselves presents two issues:

  • The first is that of perspective and the need to scale, which is dependent on how far away the viewer is from the smoke.

    A measurement of the distance between screen positions of two known points in 3D space is made at the start of the Draw() method of ReachSmoke, to help approximate the scaling of perspective.

  • The second problem is of depth, where the SpriteBatch class is more commonly called upon to draw images over the top of a 3D scene rather than within it.

    Thankfully, the Draw() method of the SpriteBatch class has an override that allows us to specify the depth of a given texture, and use this to ensure that particles appear correctly in front of, and behind other elements in a 3D scene.

There's more...

With some tweaking of the numbers and the texture, a variety of effects from flame, to steam, and even bubbles, can be achieved with a similar approach.

One addition that can really improve the realism of particle-based effects such as smoke, is animating the texture of each particle as it moves through the scene.

Examples of this can be found in games such as Naughty Dog's Uncharted 3, where they utilized an additional texture as a "movement map", where the color of each pixel in the movement map dictated the direction and intensity of the translation/distortion applied to the corresponding pixel in the texture map.

You have been reading a chapter from
Microsoft XNA 4.0 Game Development Cookbook
Published in: Jun 2012
Publisher: Packt
ISBN-13: 9781849691987
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image