忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

XNA爆発チュートリアル その7.5 ゲームコンポーネントにする

XNA爆発チュートリアルその7のつづきです。
(今回は機能を付け加えるのではなく、
リファクタリングが目的なので、
「その7.5」です。)


どうも紛糾してきたので爆発の描画をするクラスを作り、
それをGameから利用することにします。

その爆発を描画するクラスは、ゲームコンポーネント
ということにします。
Gameからやることは、そのゲームコンポーネントを
自分に登録することだけです。
ですからExplosionTest.csはうんと短くなって、
こうなります:

ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        this.Components.Add(new ExplosionRenderer(this));
    }
}



ExplosionRendererクラスは、DrawableGameComponentを
継承しており、今までExplosionTestクラスでやっていたこと、
つまり爆発の描画を行います。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            effect.Parameters["View"].SetValue(
                Matrix.CreateLookAt(
                    new Vector3(0, 0, 5), new Vector3(), new Vector3(0, 1, 0)
                    )
                );
            effect.Parameters["Projection"].SetValue(
                Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.ToRadians(45),
                    aspectRatio,
                    0.1f, 1000
                    )
                );


            GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }

    float aspectRatio
    {
        get
        {
            return (float)GraphicsDevice.Viewport.Width 
                / GraphicsDevice.Viewport.Height;
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

    public override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }
}


これでプログラムを実行しても結果は変わりません。
クラスの分離はうまく行っているようです。


さて、この時点でちょっと気持ち悪いところがあります。
それは、カメラの位置とレンズを表すViewとProjectionが、
ゲームコンポーネントの内部で決められていることです。
これではゲーム中にカメラを動かしたりが出来ません。
自分が動けないゲームなんておもしろくないでしょう。

そこで、カメラを自由に動かせるように、
カメラを表すViewとProjectionをGame側から
制御できるようにします。
こんな感じでしょうか:

ExplosionTest.cs
using Microsoft.Xna.Framework;


public class ExplosionTest : Game
{
    private GraphicsDeviceManager graphics;

    public ExplosionTest()
    {
        graphics = new GraphicsDeviceManager(this);
        ExplosionRenderer explosion = new ExplosionRenderer(this);
        Matrix view = Matrix.CreateLookAt(
            new Vector3(0, 0, 5),
            new Vector3(),
            new Vector3(0, 1, 0)
            );
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians(45),
            (float)Window.ClientBounds.Width/Window.ClientBounds.Height,
            0.1f, 1000
            );
        explosion.SetCamera(view, projection);
        this.Components.Add(explosion);
    }
}


ゲームコンポーネント側はこうです:

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();

    private Matrix view;
    private Matrix projection;


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            GraphicsDevice.VertexDeclaration = new VertexDeclaration(
                GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }


    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

    public override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );
        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();
    }

    public void SetCamera(Matrix view, Matrix projection)
    {
        this.view = view;
        this.projection = projection;
    }
}


SetCameraメソッドで直接エフェクトに値をセットできていないのは
エフェクトがまだ初期化できていない(まだnull)可能性があるからです。
変数を間接層にすることで、いつでもこのメソッドを
呼ぶことが出来るようになります。

これはなんだか意味のない間接層で、必要のない複雑性をプログラムに
組み込んでしまったような気もしますが、この程度ならまだ許容範囲ということにしておきましょう。
SetCameraメソッドを呼ぶタイミングを縛るというのもなんですしね。



最後に、ゲームコンポーネントとして再利用できるように、
別のシチュエーションで使われるときのことを考えて見ましょう。

現時点では再利用を難しくする二つの欠点があります。

1つめは、VertexDeclarationを一度だけ、LoadGraphicsContent内で
セットしていることです。
この方法はシンプルですが、再利用しようとすると問題が生まれます。
別のVertexDeclarationを使うモデルを描画した場合、上書きされてしまい
このコンポーネントの描画はうまくいかないでしょう。。

ですから、VertexDeclarationのインスタンスをDrawメソッド内で毎回セットする
必要があります。

2つ目の問題も似たようなタイプの問題で、Drawメソッドにからんでいます。
今のところDrawメソッド内でレンダーステートを色々と変更していますが、
このままだと、別のモデルを描画したときにその設定が受け継がれてしまい、
メチャクチャに描画されるでしょう。
どうしても元に戻さない設定はAlphaBlendEnableとDepthBufferWriteEnableです。

コードを書き換えて、この2つの問題を解決しましょう。

ExplosionRenderer.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


public class ExplosionRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    private Effect effect;
    private VertexDeclaration vertexDeclaration;
    private VertexVelocity[] vertices;

    private ContentManager content;
    private static System.Random random = new System.Random();

    private Matrix view;
    private Matrix projection;


    public ExplosionRenderer(Game game)
        : base(game)
    {
        content = new ContentManager(game.Services);
    }


    public override void Initialize()
    {
        vertices = new VertexVelocity[30];

        for (int i = 0; i < vertices.Length; i++)
        {
            vertices[i].Velocity = randomVector3()/10;
        }

        base.Initialize();
    }

    Vector3 randomVector3()
    {
        return new Vector3(
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1),
            (float)(random.NextDouble() * 2 - 1)
            );
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            effect = content.Load<Effect>("Content/ExplosionShader");
            effect.Parameters["ExplosionTexture"].SetValue(
                content.Load<Texture2D>("Content/explosion")
                );

            vertexDeclaration = new VertexDeclaration(
                GraphicsDevice,
                VertexVelocity.VertexElements
                );
        }
    }


    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

    public override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        GraphicsDevice.RenderState.PointSize = 50;
        GraphicsDevice.RenderState.PointSpriteEnable = true;

        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.One;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

        GraphicsDevice.VertexDeclaration = vertexDeclaration;

        effect.Parameters["Time"].SetValue(
            (float)gameTime.TotalGameTime.TotalSeconds
            );
        effect.Parameters["View"].SetValue(view);
        effect.Parameters["Projection"].SetValue(projection);

        effect.Begin();

        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
        {
            pass.Begin();

            GraphicsDevice.DrawUserPrimitives<VertexVelocity>(
                PrimitiveType.PointList,
                vertices,
                0,
                vertices.Length
                );

            pass.End();
        }

        effect.End();

        GraphicsDevice.RenderState.AlphaBlendEnable = false;
        GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
    }

    public void SetCamera(Matrix view, Matrix projection)
    {
        this.view = view;
        this.projection = projection;
    }
}



これでこのゲームコンポーネントを再利用できるようになったはずです。

拍手[0回]

PR