ゲームエフェクトデザイナーのブログ (新)

レポート記事とかUE4のマテリアルとか。C#とかも触ったり。

メインコンテンツ目次

●ニュース

タスク管理アプリ『TaskQuest』をリリースしました!

▼作ったツールやスライドはこちらで公開しています

Unreal Engine 4UE4)について書いた記事(主にVFX Artist向け)

エフェクトツールを使ってできること
エフェクトのワークフロー

テクスチャのインポートについて
テクスチャのプロパティについて
テクスチャのフォーマットについて

マテリアル作成の基本的な知識
最終マテリアル入力の各ポートについて
パーティクル向けのマテリアルの設定について

BISHAMONについて書いた記事

BISHAMONの最初のステップ
BISHAMON Personal v1.8 の新機能について
BISHAMONへのカメラのインポートについて

●Houdiniについて書いた記事

Houdiniの学習について
Billowy Smokeを使った煙のチュートリアル(前半)
Billowy Smokeを使った煙のチュートリアル(後半)

Mayaについて書いた記事

MELの最初のステップ
MELのウインドウ作成について
MELでシェーダー作成 1
MELでシェーダー作成 2
MELスクリプトのある場所を参照する
MELを配布する時のプロシージャについて
UVの正規化
頂点カラーを別のモデルに転送
テクスチャの色を頂点カラーに転送

Photoshopについて書いた記事

Photoshopのスクリプトを書いてみる

C#やプログラム全般について書いた記事

C#のはじめ方
C# ソリューションの基本的なファイル構成について

C#で正規表現による置換を行う
C# DataGridViewの主なプロパティ

C#で作ったツール一覧

GitHubでのツールやソース公開について(CGアーティスト向け)
ライセンスについてのメモ

●その他の記事

数学が苦手な人向けのCG数学シリーズ

イチオシゲームエフェクトアレコレ

処理速度と処理量のお話 ms(ミリセカンド)について
ドローコールについて

おおまかな開発コストの計算方法
おおまかなゲームソフトの売り上げの計算方法

勉強会開催のすすめ
大阪・京都の貸し会議室メモ

続きを読む

C# 穴の空いた多角形の3Dモデルにテクスチャを貼ってみた

前回に続いてテクスチャマッピングを行ってみました。
f:id:moko_03_25:20190114214652j:plain 頂点を共有していてもUVにシームを入れたい場合、1つの頂点にはUV座標は1つしか設定できないため、頂点を増やしてUVを別々に設定してあげないといけないようです。
こちらもんしょ(@monsho1977)さんにアドバイスいただいてうまくいきました!ありがとうございます!
 
コードは命名なども適当で公開するのは恥ずかしいですが、どなたかのお役に立つかもなので一応載せておきたいと思います。

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MonoGame3DTest2
{
    public class MyGame : Game
    {
        ////////////////////////////////////////////////////
        //                                               //
        // 穴の空いた円ポリゴンにテクスチャを貼るサンプル //
        //                                               //
        ////////////////////////////////////////////////////

        GraphicsDeviceManager graphics;
        BasicEffect effect;

        static int polyCircleCount = 48;
        float r = 0.3f;
        // UVのシーム用に2箇所で2重に頂点を生成するため+2している
        VertexPositionTexture[] vertices = new VertexPositionTexture[polyCircleCount * 2 + 2];
        int[] indices  = new int[polyCircleCount * 2 * 3];
        
        // テクスチャ
        Texture2D texture;

        // カメラの制御用
        float angle = 0;
        float height = 0;

        public MyGame()
        {
            graphics = new GraphicsDeviceManager(this);
        }

        protected override void LoadContent()
        {
            // 多角形の頂点情報
            for (int i = 0; i < polyCircleCount + 1; i++)
            {
                // 円を分割した1つ分の角度
                double cut = (Math.PI * 2) / polyCircleCount;
                // 角度
                double theta = cut * i;

                // X, Y の算出
                float x2 = (float)(Math.Cos(theta));
                float y2 = (float)(Math.Sin(theta));
                float x1 = r * x2;
                float y1 = r * y2;

                // UVのU座標の算出
                float u = 1.0f / polyCircleCount * i;

                // 円の内側の頂点
                vertices[i] = new VertexPositionTexture
                    (
                        new Vector3(x1, y1, 0),
                        new Vector2(u, 1)
                    );

                // 円の外側の頂点
                vertices[i + polyCircleCount + 1] = new VertexPositionTexture
                    (
                        new Vector3(x2, y2, 0),
                        new Vector2(u, 0)
                    );

                // デバッグ用
                //Console.WriteLine("U: " + u);
                theta = (theta / Math.PI) *180;
                float dX1 = (float)(((double)x1 / Math.PI) * 180);
                float dY1 = (float)(((double)y1 / Math.PI) * 180);
                float dX2 = (float)(((double)x2 / Math.PI) * 180);
                float dY2 = (float)(((double)y2 / Math.PI) * 180);
                Console.WriteLine("i: " + i + " theta: " + theta +
                " x1: " + dX1 + " y1: " + dY1 +
                " x2: " + dX2 + " y2: " + dY2);
            }

            // 穴の空いた円ポリゴンのインデックス生成
            int vertexId = 0;
            for (int i = 0; i < polyCircleCount; i++)
            {
                // 一度に三角形2つ分のIndexを指定するので6ずつ増やす
                int i2 = i * 6;

                // 三角形1つ目
                indices[i2]     = vertexId;
                indices[i2 + 1]    = vertexId + polyCircleCount + 1;
                indices[i2 + 2]    = vertexId + polyCircleCount + 2;
                // 三角形2つ目
                indices[i2 + 3]    = vertexId;
                indices[i2 + 4]    = vertexId + polyCircleCount + 2;
                indices[i2 + 5]    = vertexId + 1;

                vertexId++;
            }

            effect = new BasicEffect(GraphicsDevice)
            {
                TextureEnabled = true,

                Projection = Matrix.CreatePerspectiveFieldOfView
                (
                    MathHelper.ToRadians(45),              // 視野角
                    GraphicsDevice.Viewport.AspectRatio,    // アスペクト比(横/縦)
                    1,     // ニアクリップ
                    100        // ファークリップ
                )
            };

            // 画像を読み込んでテクスチャに設定
            texture = Content.Load<Texture2D>("Content/sample");
        }

        protected override void UnloadContent()
        {
            effect.Dispose();
        }

        protected override void Update(GameTime gameTime)
        {
            KeyboardState keyboardState = Keyboard.GetState();
            if (keyboardState.IsKeyDown(Keys.Left))
                angle -= MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            if (keyboardState.IsKeyDown(Keys.Right))
                angle += MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            if (keyboardState.IsKeyDown(Keys.Up))
                height += 0.1f; // 1/60秒に0.1移動
            if (keyboardState.IsKeyDown(Keys.Down))
                height -= 0.1f; // 1/60秒に0.1移動
        }

        protected override void Draw(GameTime gameTime)
        {
            // 背景のクリア
            Color c = new Color(100, 110, 60);
            GraphicsDevice.Clear(c);

            // テクスチャの設定
            effect.Texture = texture;

            // 両面描画
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;

            // カメラの位置
            effect.View = Matrix.CreateLookAt
            (
                new Vector3
                (
                    3 * (float)Math.Sin(angle),
                    height,
                    3 * (float)Math.Cos(angle)
                ),
                new Vector3(0, 0, 0),   //カメラの注視点
                new Vector3(0, 1, 0)    //カメラのUPベクトル
            );

            // 三角形の描画
            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                // 3角形を実際に描画する
                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionTexture>(
                    PrimitiveType.TriangleList,
                    vertices,
                    0,
                    vertices.Length,
                    indices ,
                    0,
                    indices .Length / 3
                    );
            }
        }
    }
}

 
例によってこの記述ではポリゴンの面が裏を向いて生成されてしまっていると思います。
(両面描画にしているので表示されていますが)

C# 穴の空いた多角形の3Dモデルの描画

前回の多角形ポリゴンに引き続いて、MonoGame(XNA)を使ってC#で穴の空いた多角形ポリゴンを描画してみました。
f:id:moko_03_25:20190114151255g:plain

こういうパイナップル型のモデルを何と呼べば良いんでしょう。リング?
 
「多角形の分割数」を変えれば、四角形>五角形>六角形‥と増やしていけます。
頂点インデックスは法則を見つけて落とし込む感じですね。。

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MonoGame3DTest2
{
    public class MyGame : Game
    {
        ////////////////////////////////////////
        //                                   //
        // 穴の空いた円ポリゴンを描くサンプル //
        //                                   //
        ////////////////////////////////////////

        GraphicsDeviceManager graphics;
        
        // データがどのように描画されるかを制御するクラス
        BasicEffect effect;

        // 多角形の分割数
        static int polyCircleCount = 12;
        // 中心の穴の半径
        float r = 0.3f;

        // 座標と色の2つのデータを持った頂点情報の構造体
        VertexPositionColor[] vertices = new VertexPositionColor[polyCircleCount * 2];

        // 頂点インデックス指定用
        int[] indices  = new int[polyCircleCount * 2 * 3];
        
        // カメラの制御用
        float angle = 0;
        float height = 0;

        // コンストラクタ
        public MyGame()
        {
            graphics = new GraphicsDeviceManager(this);
        }

        // 表示物の準備
        protected override void LoadContent()
        {
            // 円の頂点情報
            for (int i = 0; i < polyCircleCount; i++)
            {
                // 円を分割した1つ分の角度
                double cut = (Math.PI * 2) / polyCircleCount;
                // 角度
                double theta = cut * i;

                // X, Y の算出
                float x2 = (float)(Math.Cos(theta));
                float y2 = (float)(Math.Sin(theta));
                float x1 = r * x2;
                float y1 = r * y2;

                // 円の内側の頂点
                vertices[i] = new VertexPositionColor
                    (
                        new Vector3(x1, y1, 0),
                        Color.White
                    );

                // 円の外側の頂点
                vertices[i + polyCircleCount] = new VertexPositionColor
                    (
                        new Vector3(x2, y2, 0),
                        Color.White
                    );

                // デバッグ用
                //theta = (theta / Math.PI) *180;
                //float dX1 = (float)(((double)x1 / Math.PI) * 180);
                //float dY1 = (float)(((double)y1 / Math.PI) * 180);
                //float dX2 = (float)(((double)x2 / Math.PI) * 180);
                //float dY2 = (float)(((double)y2 / Math.PI) * 180);
                //Console.WriteLine("i: " + i + " theta: " + theta +
                //" x1: " + dX1 + " y1: " + dY1 +
                //" x2: " + dX2 + " y2: " + dY2);
            }

            // 穴の空いた円ポリゴンのインデックス生成
            int vertexId = 0;
            for (int i = 0; i < polyCircleCount; i++)
            {
                // 一度に三角形2つ分のIndexを指定するので6ずつ増やす
                int i2 = i * 6;

                // 最後のポリゴン生成かどうか判定
                if (vertexId != polyCircleCount - 1)
                {
                    // 三角形1つ目
                    indices[i2]     = vertexId;
                    indices[i2 + 1]    = vertexId + polyCircleCount;
                    indices[i2 + 2]    = vertexId + polyCircleCount + 1;
                    // 三角形2つ目
                    indices[i2 + 3]    = vertexId;
                    indices[i2 + 4]    = vertexId + polyCircleCount + 1;
                    indices[i2 + 5]    = vertexId + 1;
                }
                else
                {
                    // 三角形1つ目
                    indices[i2]     = vertexId;
                    indices[i2 + 1]    = vertexId + polyCircleCount;
                    indices[i2 + 2]    = polyCircleCount;
                    // 三角形2つ目
                    indices[i2 + 3]    = vertexId;
                    indices[i2 + 4]    = polyCircleCount;
                    indices[i2 + 5]    = 0;
                }

                // デバッグ用
                //Console.WriteLine("i: " + i + " vertexId: " + vertexId);

                vertexId++;
            }

            effect = new BasicEffect(GraphicsDevice)
            {
                Projection = Matrix.CreatePerspectiveFieldOfView
                (
                    MathHelper.ToRadians(45),              // 視野角
                    GraphicsDevice.Viewport.AspectRatio,    // アスペクト比(横/縦)
                    1,     // ニアクリップ
                    100        // ファークリップ
                )
            };
        }

        // メモリからの解放
        protected override void UnloadContent()
        {
            effect.Dispose();
        }

        // 計算の更新
        protected override void Update(GameTime gameTime)
        {
            // キーボード制御
            KeyboardState keyboardState = Keyboard.GetState();

            if (keyboardState.IsKeyDown(Keys.Left))
            {
                angle -= MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            }
            if (keyboardState.IsKeyDown(Keys.Right))
            {
                angle += MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            }
            if (keyboardState.IsKeyDown(Keys.Up))
            {
                height += 0.1f; // 1/60秒に0.1移動
            }
            if (keyboardState.IsKeyDown(Keys.Down))
            {
                height -= 0.1f; // 1/60秒に0.1移動
            }
        }

        // 描画
        protected override void Draw(GameTime gameTime)
        {
            // 背景のクリア
            Color c = new Color(100, 110, 60);
            GraphicsDevice.Clear(c);

            // 両面描画
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;

            // カメラの位置
            effect.View = Matrix.CreateLookAt
            (
                new Vector3
                (
                    3 * (float)Math.Sin(angle),
                    height,
                    3 * (float)Math.Cos(angle)
                ),
                new Vector3(0, 0, 0),   //カメラの注視点
                new Vector3(0, 1, 0)    //カメラのUPベクトル
            );

            // 三角形の描画
            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                // 3角形を実際に描画する
                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(
                    PrimitiveType.TriangleList,
                    vertices,
                    0,
                    vertices.Length,
                    indices ,
                    0,
                    indices .Length / 3
                    );
            }
        }
    }
}

 
内側と外側で円状に配置する頂点は 半径Cos(角度) と 半径Sin(角度) でXとY座標を出しています。
(外側は 半径=1 にしているため r* は省略しています)
 
例によってこの記述ではポリゴンの面が裏を向いて生成されてしまっていると思います。
(両面描画にしているので表示されていますが)

C# 多角形の3Dモデルの描画

MonoGame(XNA)を使ってC#で多角形ポリゴンを描画してみました。
MonoGame 自体に関してはこちらで紹介しています。
f:id:moko_03_25:20190114124710g:plain  
かんたんXNA4.0 (XNA入門)|Memeplexes
ポリゴン生成などの基本的な記述に関してはこちらのサイトを参考にさせていただいています。
 
「多角形の分割数」を変えれば、四角形>五角形>六角形‥と増やしていけます。
頂点インデックスを生成するのが大変ですね。。

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MonoGame3DTest2
{
    public class MyGame : Game
    {
        //////////////////////////////
        //                         //
        // 円ポリゴンを描くサンプル //
        //                         //
        //////////////////////////////

        GraphicsDeviceManager graphics;
        
        // データがどのように描画されるかを制御するクラス
        BasicEffect effect;

        // 円の分割数
        static int polyCircleCount = 12;

        // 座標と色の2つのデータを持った頂点情報の構造体
        VertexPositionColor[] vertices = new VertexPositionColor[polyCircleCount + 1];

        // 頂点インデックス指定用
        int[] indices = new int[polyCircleCount * 3];
        
        // カメラの制御用
        float angle = 0;
        float height = 0;

        // コンストラクタ
        public MyGame()
        {
            graphics = new GraphicsDeviceManager(this);
        }

        // 表示物の準備
        protected override void LoadContent()
        {
            // 中心点
            vertices[0] = new VertexPositionColor(new Vector3(0, 0, 0), Color.White);

            // 円の外周
            for (int i = 1; i <= polyCircleCount; i++)
            {
                // 円を分割した1つ分の角度
                double cut = (Math.PI * 2) / polyCircleCount;
                // 角度
                double theta = cut * (i - 1);
                // X, Y の算出
                float x = (float)Math.Cos(theta);
                float y = (float)Math.Sin(theta);

                vertices[i] = new VertexPositionColor
                    (
                        new Vector3(x, y, 0),
                        Color.White
                    );

                // デバッグ用
                //theta = (theta / Math.PI) *180;
                //float dX = (float)(((double)x / Math.PI) * 180);
                //float dY = (float)(((double)y / Math.PI) * 180);
                //Console.WriteLine("cut: " + cut + " d; " + theta + " X: " + dX + " Y: " + dY);
            }

            // 円ポリゴンのインデックス生成
            int vertexId = 0;
            for (int i = 0; i <= (polyCircleCount - 1) * 3; i += 3)
            {
                // 中心
                indices[i]      = 0;

                // 外側1
                indices[i + 1] = vertexId + 1;

                // 外側2
                if (vertexId != polyCircleCount - 1)
                {
                    indices[i + 2] = vertexId + 2;
                }
                else
                {
                    indices[i + 2] = 1;
                }
                vertexId++;

                // デバッグ用
                //Console.WriteLine("i: " + i + " c: " + vertexId);
            }

            effect = new BasicEffect(GraphicsDevice)
            {
                Projection = Matrix.CreatePerspectiveFieldOfView
                (
                    MathHelper.ToRadians(45),              // 視野角
                    GraphicsDevice.Viewport.AspectRatio,    // アスペクト比(横/縦)
                    1,     // ニアクリップ
                    100        // ファークリップ
                )
            };
        }

        // メモリからの解放
        protected override void UnloadContent()
        {
            effect.Dispose();
        }

        // 計算の更新
        protected override void Update(GameTime gameTime)
        {
            // キーボード制御
            KeyboardState keyboardState = Keyboard.GetState();

            if (keyboardState.IsKeyDown(Keys.Left))
            {
                angle -= MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            }
            if (keyboardState.IsKeyDown(Keys.Right))
            {
                angle += MathHelper.ToRadians(2.0f); // 1/60秒に0.5°回転
            }
            if (keyboardState.IsKeyDown(Keys.Up))
            {
                height += 0.1f; // 1/60秒に0.1移動
            }
            if (keyboardState.IsKeyDown(Keys.Down))
            {
                height -= 0.1f; // 1/60秒に0.1移動
            }
        }

        // 描画
        protected override void Draw(GameTime gameTime)
        {
            // 背景のクリア
            Color c = new Color(100, 110, 60);
            GraphicsDevice.Clear(c);

            // 両面描画
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;

            // カメラの位置
            effect.View = Matrix.CreateLookAt
            (
                new Vector3
                (
                    3 * (float)Math.Sin(angle),
                    height,
                    3 * (float)Math.Cos(angle)
                ),
                new Vector3(0, 0, 0),   //カメラの注視点
                new Vector3(0, 1, 0)    //カメラのUPベクトル
            );

            // 三角形の描画
            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                // 3角形を実際に描画する
                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(
                    PrimitiveType.TriangleList,
                    vertices,
                    0,
                    vertices.Length,
                    indices,
                    0,
                    indices.Length / 3
                    );
            }
        }
    }
}

 
中心の頂点だけ座標が(0, 0, 0)で、外周の頂点は Cos(角度) と Sin(角度) でXとY座標を出しています。
 
ちなみにこの記述では裏を向いて生成されてしまっていると思います。
(両面描画にしているので表示されていますが)

C# MonoGameで3Dモデルを表示してみる

こちらの記事にまとめてるように「Maneged DirectX」や「SlimDX」の解説記事をもとに3Dモデルの表示を片っ端から試してみましたが、途中でうまくいかなくなったりコードが複雑で理解が追いつけなかったりしました。

そんな中、DirectXよりかなり敷居が低いと言われているXNAマイクロソフト公式でそのまま動くよう移植したという「MonoGame」が良さそうに思い、XNAの解説を頼りに学習してみることにしました。

その際に躓いたことなどメモっておきたいと思います。※随時更新

続きを読む

Making of TaskQuest

TaskQuest」をどんな感じで作っていったか軽くまとめておきたいと思います。

 2019.1.11 実装編も書きました!
 Making of TaskQuest<実装編> - ゲームエフェクトデザイナーのブログ (新)

 

制作の動機


これまでちょっとしたタスク管理を個人的に行う際に、メモ帳などのテキストエディタで行ったり、エクセルで行ったり、アナログな付箋紙で行ったりしていましたが、どれも不便に感じていました。

テキストエディタだと、進行中のタスクや完了したタスクのグループ分けを手作業でやるというのは非効率すぎます。。

エクセルのソートやフィルタでそのあたりは吸収できますが、以下のような不満があります。

・起動にちょっと時間がかかる
・タスク管理面では必要以上にメニュー等が多くて余分な面積を取る
 なのでウインドウサイズを小さくして画面の隅っこに置いて使うには不向き
・「完了」などタスクのステータスに応じたフィルタリングの操作がやや煩雑
  ⇒ こちらはVBAでボタン作ったりすればフォローできますね


そこで「Backlog」のようなすっきりしたルックでWindows用のスタンドアローンなツールがあればなあと思っていました(Webベースだと個人でラフに使う分には、リストのコピペなどレスポンス良くできずあまり好みでは無いので)。

C#Windowsフォームアプリを作るようになってからは、タスク管理アプリも「いつか作りたいな」といった感じで制作候補の1つになっていました。

ちなみに、ドット絵のキャラが成長する要素は最初から入れたいと思っていました。

例えば「Yahoo!知恵袋」でもスコアを稼いでグレードを上げていくようなシステムが入っていますが、こういったゲーミフィケーション的な要素は世の中の色んなものに入っていて欲しいと昔から思っていたので、タスク管理を楽しくするためにも欲しい機能だなあと。

制作開始したきっかけ


とは言え、Windowsフォームで使えるコントロール(ボタンやリストなどのGUIパーツ)を使ってどうしたら再現できるのか悩んでいました。

当時は「ListFusen」のように2カラムなスタイルにして、左側にはタスクをリスト上に並べて、クリックすると右側に詳細が表示されるような形かな~となんとなく思っていましたが、こちらでも紹介している書籍「作って覚えるVisual C# 2017 デスクトップアプリ入門」の家計簿アプリをあらためてなぞってみた際に「家計簿アプリのアレンジでいける!」と思って、具体的にアプリの中身を考え始めました。

草案みたいなもの


スマホのメモアプリに最初にざーっと必要な要件を書き出したのがこちらです(長い!)。

タスク管理アプリ「Task Quest」

◆トップ画面

今やっているタスクをトップに表示する?
→ 悪くないが頻繁にアプリにアクセス必要になる 面倒がられそう

そしてタスクリストは普段消しておき、ボタン押下ですぐリストの開閉ができるようにする?
→ 同上

また、今やってるタスク表示の横に完了ボタンを置くとなお快適に?
→ 完了ボタンをトップに置くのはありかも 但し実工数を付け忘れそう

タスクが終わったら完了にする流れはこの方が習慣付けやすいかも。
また、画面もスッキリする。

敵と戦ってるようなキャラアニメも乗せたい

アイコンは
・新規タスク追加
・選択タスクの削除
・完了にする

情報は
・タスク総数 0 / 0 (個)
・総工数 0 / 0 (日)
・Lv
・経験値 見せない方が良いかも

◆リストの挙動

タスクをダブクリで設定画面が開く
タスクの難易度に応じた敵アイコン
概要欄は文字数を制限して省略する

◆リストの表示モード

フィルタは完了以外、完了、すべて
大カテゴリでのフィルタも?

検索ありにする?
大カテゴリ、内容、ID、備考で可能?

◆設定画面

ボタンはOK、トップに表示、完了

完了時には実際の工数を入力させる?
オプションで選べると良さそう
最初に設定した工数と差分を出して見積もり精度を表示する?

◆リスト

大カテゴリ プルダウン?
内容
ID
難度 プルダウン 固定
工数
進捗 プルダウン 固定
備考

カテゴリのプルダウンはどうする?
ありの方が快適になるが、後から変更すると一気に全て変わってしまうが‥
あると登録時には快適になる
しかしカテゴリ登録の手間は増える
カテゴリ登録の画面と設定保存が必要
とりあえず無し?

設定ファイル書き換えも可能にする?

◆レベル

累計の経験値でレベルが決まる
タスク完了時にレベルアップ判定
専用のメッセージと演出を入れたい

経験値
タスク登録
・基礎10 * 難度 * 工数h
・難度倍率は 1, 2, 3
例:難易度中の8時間作業
10 * 2 * 8 = 160
・難度は含めない方が良いかも
工数は時間単位
工数は含めない方が良いかも
固定値でいくなら基礎ポイントは高め
50くらい?
タスク最初に20個登録して1000
1タスク平均3hとして1日3タスク消化
150取得、1週間750、1ヶ月3000
1年36000 タスク登録だけで

タスク変更
・ひとまず考慮しない?
・ちゃんと差分を増減させる?
・タスク完了を差し戻した場合は考慮しないといけない
何もしなくても経験値を増やせちゃう

タスク削除、未完
・ひとまず考慮しない?
・登録と同じ経験値をマイナスする?

タスク完了
・基礎30 * 難度 * 実工数
・難度倍率は 1, 2, 3
例:30 * 2 * 8 = 480
難度小1日240、中480、大720
難度中で1週間2400、1ヶ月9600
1年115200
タスク登録を足すと151200

細かくタスクを登録した方がメリット持たせるには、タスク登録は固定の経験値が良いかも
実装も計算もシンプルになる
レベルダウンも無くせる wiz..

予定より早く終わらせた場合は?
ボーナス?
見積もり精度の方が重要かも

見積もり精度も考慮させたい
見積もり精度ボーナス?
見積もり精度の計算
工数を1として実工数の割合を出す
1はボーナス * 1.2
0.8〜1.2はボーナス *1.1

全タスク完了ボーナス?
タスク沢山登録するメリット無くなる

どれくらいLvUpさせる?
1年使ってくれたら万々歳すぎる
Lv50くらい?
1年の経験値が150000としたら
単純に均等割りで1Lvに3000
対数的にする?
最初だけはサクサク上げたい
使い始めの初日、200でLv2とか
500でLv3、900でLv4、1400でLv5
タスク20個登録の難度中8hでLv5

◆レベルアップ時の演出

ファンファーレを入れたい
メッセージを入れたい
・レベル、タスク総数、総工数
・キャラステータス持たせる?
ランダム要素に上がりやすさを加える
難度(優先度)高い 力
難度(優先度)低い 素早さ
工数大きい 体力 3h〜くらい?
工数小さい 素早さ 1〜2hくらい?
コメント書いてたら 運の良さ
見積もり精度高いと 賢さ
二律背反は無いが、力と体力どちらも高い時は素早さが低い

レベルアップ時
優先度 80% 力or素早さ+1 20% 無し
工数 80% 体力or素早さ+1 20% 無し
コメント 80% 運+1 20% 無し
総合見積もり精度を出す
70%以上で賢さ+1 満たないと無し

ステータスの偏りでグラ変える?
一定条件で忍者/ロード/侍とか wiz

○下級クラス
Lv10以上
見積もり精度関係なし
最も高いステータスの割合で決まる
力 = ウォーリア
体力 = クレリック
素早さ = ローグ

○中級クラス
Lv20以上
見積もり精度関係なし
力 + 体力 = ナイト
力 + 素早さ = アーチャー
体力 + 素早さ = ビーストテイマー

○上級クラス
Lv30以上
見積もり精度が高い役職
特定の値を超えてかつ割合で決まる
見積もり精度が満たない場合は中級
賢さ + 力 = パラディン
賢さ + 体力 = ドラグーン
賢さ + 素早さ = ウォーロック
賢さ + 力 + 体力 = ○○○○
賢さ + 力 + 素早さ = ○○○○
賢さ + 体力 + 素早さ = ○○○○

○その他
Lv10以上 運が一番高い = ○○○○
Lv30以上 運が一番高い + 賢さ = ○○○○
確率的にはレアにしたい
バランス良いタスクで几帳面だとなる
可能性がある感じ

◆難度の定義

カンタン、ふつう、ムズい

カンタンは単純作業
リスト記入、報告、会議出席
バウンディングボックス設定

ふつうは普段の制作作業
テクスチャ、モデル、エフェクト

ムズいは新表現の模索、検証など
成功するか不安なもの
できた時に仲間から尊敬される作業

◆優先度


ほかの作業より先に提出する必要あり
作業の遅れが開発全体の遅れに直結


普通の作業


後回しにしてOK
または最悪やらなくて良い 努力目標

◆名前と性別の入力

必要に思う!
性別はナイーブなので扱わない?
でもあった方が良い
しかしグラフィックは倍大変

変数を用意
Strings name;
Bool male;
初めてアプリ起動時に聞く

オプション設定から変更できる

◆経験値取得とレベルアップの実装

○経験値取得処理
int exp;
只々足していき、消費やマイナス無し

・タスク追加時 50固定
exp += 50;
経験値バー更新

・タスク削除時 何も無し

・タスク完了時
モンスターを倒すグラフィック再生
優先度と工数に応じて変わる
exp += 30 * *
次にレベルアップ判定に行く

○レベルアップ処理
int lv;
こちらも只々足していく
int lvupNext = lv+ 1;
int lvupNow = lv;
int lvupNextExp =
int lvupNowExp =
必要経験値 lvupNext - lvupNow
現在経験値 exp - lvupNowExp
現在 ÷ 必要 * 100 = %

・タスク完了時
While 条件を満たさなくなるまで
次のLvUp条件を満たした
経験値バーを一旦100%にする
グラフィック表示 + SE
メッセージ
○○は Lv 00 になった!
○○が ○ あがった
LvUpしたフラグON
Break;
ジョブチェンジ判定
今は何のジョブになるか?
今と同じジョブなら終了
別のジョブなら
グラフィック表示 + SE
メッセージ
○○ は ○○ にジョブチェンジ
ジョブチェンジしたフラグON
終了判定
経験値バー更新
フラグに合わせてヒントを表示
"工数を入力すると良いことが!"
"見積もりに近いと良いことが!"
"ステータスでジョブが変わる!"
"タスクは細かく分けよう!"
フラグを全てOFF
OKボタンを表示(消しておく)

◆グラフィックデータ

下記 * 性別分

デフォルトの絵 赤ちゃん Lv1専用
冒険者見習い Lv2〜9まで
ジョブ14種類
計16種

敵の種類
優先度3 * 工数3 * バリエ3 = 27種
バリエはレベルに合わせる?

倒す演出エフェクト GIFアニメ重ねる
殴り、一閃、フラッシュ

消滅エフェクトきらきら GIFアニメ

ボタンアイコン 6種
追加、編集、検索、複製、削除、歯車

◆保存

CSVの形で保存
なのでエクスポート機能は無し

◆おまけ要素

○アプリ情報

メールアドレス
ブログ
ツイッターとアイコン
作成日とバージョン
バージョンアップボタンで対応

○デジタル証明書

プロジェクトのアセンブリ情報のプロパティで設定できる模様?

○未処理で完了

モンスターが逃げる演出とSE

○カラーチェンジ

オプションで色変え
帯の色を自由に変えられる
アイコンは中間色にする必要がある
Lv10で解放とか?

○ジョブ図鑑

オプションでレベルと経験値をリセットできるようにする

ジョブ図鑑でなったジョブが表示

基本ジョブが全て埋まると
勲章アイコンが画面に付く
全ジョブが埋まると
マスターアイコンが画面に付く

◆リリース方法

Booth
500円でどうか

GUMROAD
お値段を決めて買ってもらうサービス
簡単な1ページ説明書
おまけでメイキングPDF付ける?


ざっと見ても分かる通り、タスク管理としての機能よりもキャラ成長要素の仕様をどうするかのメモの方が多いです。楽しいですね。。

ちなみに、大体のRPGではレベルが上がるほどに必要な経験値が増えますが、タスクから得られる経験値が一定なのと、使用して月日が経つほどにレベルアップの頻度が落ちていくのは寂しいと思い、レベルが上がっていってもある程度一定のペースで上がり続けていくバランスに落とし込みました。Wizardryのようにレベル1000とか上がってもいいじゃない。

また、ジョブはネタバレになるので一部伏せていますが、元ネタとしては「Wizardry」「Tastics Ogre」「FF」あたりが混ざっています。忍者とかロードとか入れたいですよね!

それからオミットとなったアイデアも色々とあります。TaskQuestというからにはタスクも敵キャラとしてドット絵を用意して戦闘画面風の演出を入れたかったりはしましたが、「完成がかなり遠のきそうだ」と思って諦めました。。

こちらが、上記メモを書いていた時のツイートです。

 

制作開始!


まずはコントロールを配置しながら大枠のデザインを決めて、エクセルの表のような「DataGridView」というコントロールに、タスクを登録できるところまで実装しました。

会社から帰宅したら作るという感じで1週間ほどすると根幹となる機能は入りましたが、まだまだ実装を予定している機能が大量にあり、また色んな不具合や不便な要素に困っている状態でした。

 

成長要素の導入とクラス分け


1つのCSファイルにズラズラっと全ての変数や関数を書いていると可読性が下がり大変になってきたので、クラス分けの先駆けとして「ツール設定を管理するクラス」「キャラのステータスを管理するクラス」を用意して移し換えました。

実はここで初めて「プロパティ」を試しました。

プロパティという単語自体はアーティストでも色んなツールを使う上で普段から慣れ親しんでいるものですが、プログラム初心者の身からすると変数と何が違うのかよく分からない訳です。でもとりあえず使ってみようと(結局、クラス内で変数のようにしか扱っていないので変数で良かったかなという感じではありますが)。

この頃、C#を触り始めて1年半ほどでツール制作にも徐々に慣れてきてはいましたが、仕事ではコーディングしないのであくまで趣味の範疇です。まだまだ文法は知らないことばかりで初めて試すものも多く、また「構造体」「インターフェース」「デリゲート」など全く分からない状態でした。

その時の気持ちはこちら。

また設定を管理するようなクラスはアクセス修飾子をstaticにしてメモリに常駐させ、newしなくてもいつでもプロパティの中身を読み書きできるようにすると便利ということに気付いて、後から全てstaticに書き替えたりしました。

それまでクラスのアクセス周りはよく分からずに困ることが多かったのですが、例えば子となる別のフォームから親となるメインフォームを制御したい場合に、メインフォーム自身をコンストラクタの引数として渡してあげたら可能だというのも今回の制作でようやく理解した感じです。それも「多分スマートではない方法なんだろうな」と思いつつ、他に方法を知らないのでとりあえずメインフォームやそれに属するコントロールを別のフォーム生成時にコンストラクタで渡すというのを非常に多用しています。

さて、下記のような成長要素を暫定で入れましたが、このようにメッセージボックスを表示するだけなら非常に楽です。しかし、後にちゃんとデザインした独自のフォームに表示させようとした際に中身が複雑になってかなり苦労しました。

 

色々な機能を実装


必須機能として「完了以外」「完了」などのステータスに応じたフィルタリングを入れましたがここでかなり躓き、データベース・DataSet・DataTableと、それらの値を表示するDataGridViewとのやり取りの基礎的なあたりを色々調べたりしました。

また、複数セルへの一括入力や一括ペーストにも対応しました。こちらは必須だろうと。

しかしDataGridViewのイベントを取得するタイミングが色々とあるものの、それぞれのタイミングで色んな問題が噴出し、不具合に悩まされることとなります。

f:id:moko_03_25:20190110163500g:plain

キャラステータス画面やオプション画面も入りました。

こういった縦横に自由に敷き詰めるレイアウトで画面を作ると、設定項目が1つ増減するだけでレイアウトに大きく影響するのであまりよく無いですね。。

f:id:moko_03_25:20190110163604g:plain

色を選択するためのフォームを作ったり‥

f:id:moko_03_25:20190110163717g:plain

初起動時の設定画面を作ったり‥

f:id:moko_03_25:20190110164156j:plain

ジョブ図鑑を作りました。ジョブを新たに取得するごとにシルエットがオープンになる感じです。

こういったオマケ的な要素は、デザインするのは楽しいですが実装すること自体は単純なので面倒ですね。。気分転換したい時にはドット絵を修正したりしていました。

f:id:moko_03_25:20190110164237g:plain

タスク完了>レベルアップ>ジョブチェンジの演出のためのフォームを作って差し替えましたが、RPGメッセージ風にテキストを表示するために同期&非同期処理について調べたり、GIFアニメ画像を載せたりするためにコントロールのレイヤー周りについて調べたりして結構大変でした。。

結局のところ「Windowsフォームは制約が多すぎて、コントロールに重なるようにビジュアル的な演出を入れるのには向かない」という結論に達し、ここは色々と割り切りました。残念です。。

f:id:moko_03_25:20190110170949g:plain

下図は After Effects で作ったイメージ画像ですが、たったこれだけのこと(コントロールにまたがるようにGIF画像を上に載せて表示する)ができないんですよね。。

f:id:moko_03_25:20190110171616g:plain

という訳で、着手を始めて1ヶ月ほどであらかたの機能が入りました。

この後にも実行ファイルにアイコンを適用したり保存データを暗号化したり不具合を取り除いたりと、1ヶ月ほど追加でちょこちょこと作業はありましたが、大体の機能はこのあたりで入った感じです。

 

テストしてくれる方とslackでやり取り


今回は当初より「販売する」というのが1つの試みでもあったので、不具合満載の商品をリリースする訳にもいきません。そこで会社の同僚やTwitterのフォロワーさんに何人かにテストをお願いして触ってもらい、不具合報告や要望などをいただきながら開発を進めていました。

ありがたいことにTwitterですぐにお声掛けをいただき、slackで専用のプロジェクトを用意してそこでやり取りさせていただきました。

f:id:moko_03_25:20190110173033j:plain

最新の実行ファイルもどんどんアップしていけますし、とても便利でした。

分からない時にどうしたのか


大体はココナラで有料で質問して解決しています。
有料だと気兼ねなく聞けるのが良いですね!

いつもお世話になっているのはこちらの「Kazuki Takayama」さんになります。

他にも、ある不具合の原因が分からずに困っていた時 sw(@Callisto_n)さんに泣き付いて、勉強会の帰りにカフェで不具合を見つけていただいたのですが、その際にソースコードをざっと見渡して改善ポイントまで教えていただいたりしました(swさんその節はありがとうございました‥!)。

諦めたこと


一番大きいところでは、複数人でのタスク管理です。

「TaskQuest」はあくまで個人のちょっとしたタスク管理を想定しているため、お仕事で使うには正直厳しいと思っています。特に複数人で1つのプロジェクトを同時編集したいと思った場合には、恐らく以下のようなあたりの実装が必要なのかなと‥。

こちらは今の自分には厳しいと思って諦めました。

・データベースの知識
 サーバ上でリストを管理して、クライアントのアプリからアクセスする形

・アカウントを登録&管理するような仕組み
 パーミッションとかも含めた色々な設定

複数アカウントの同時編集の仕組み


こちらに関しては「Googleドキュメントなど元からクラウド上で複数人の編集が可能になっているサービスを利用すれば楽にできるのでは?」というアドバイスをいただいたことがあり、それならもしかしたら手軽にいけたりするんでしょうか。また機会があれば試してみたいです。

次に、スマホアプリとしてのリリースです。

こちらは「Xamarin」とか使う感じでしょうか。軽く調べた感じでは面白そうではあるのですが、今の自分にはまだまだ敷居が高そうな印象がありました。でもそのうち試してみたくはあります。

それから、アンドゥとリドゥの実装。

C#でUndo/Redoを実装した - Qiita

こちらで概念から解説されていて「これは今の自分には無理だ」と即諦めました。。

複数行の一括削除。

リストと複数セルの選択ができるのだから、複数行を一括削除もしたいものと思います。でもこれはアンドゥとリドゥが実装されている前提になると思いました。うっかりミスで複数行を削除してしまった場合に悲劇となります。。なのでこちらも同時に諦めました。

全タスクの文字列の検索・フィルタリング・置換

データベースとDataGridViewの理解が怪しいため不具合の温床になりそうで諦めました。。

 

販売してみる


今回は自分が作ったプログラムを初めて販売してみました。

世の中に非常に優秀なフリーソフトが大量にあることは勿論知っていますし、例え50円や100円のお値段設定だとしてもクレジットカード決済やコンビニ振り込み等のハードルから触ってもらえる機会が大幅に失われるだろうと思いましたし、「素人が作った不出来なものを売って良いのか?」という自問もありましたが、「どれくらいの人に買ってもらえるのだろう?」という純粋な興味が大きくありました。

目標としては40人の方にご購入いただいて2万円を売り上げることです。
そうすれば、C#関連書籍の購入とココナラの取り引きの費用などを回収できる感じです。

現在リリース10日ほどで18本売れたので、このまま40本達成できたら嬉しいなという感じです。

C#で3Dモデルを表示したい場合の方法メモ

雑なメモ ※随時更新

Managed DirectXを使う方法


Managed DirectX というもののおかげで C# でも DirectX を扱うことができるようです。

怖いものなんてない!!: C#で「using Microsoft.DirectX;」をすると参照エラーになる

 Visual Studio Community 2017をインストールするとDirectX SDKも同時にインストールされている様子。しかしusingディレクティブでDirectX関連のネームスペースを指定してもエラーになるので、関連するDLLファイルを直接参照してあげる必要があるようです。

C#でゲームプログラム講座 - DirectX - DirectGraphics①

 DirectXでゲーム用に3Dモデルを描画することについて丁寧に説明されています。
 ただしテクスチャを貼ってアルファブレンディングするところで更新が止まっています。

ステップアップ (Managed DirectX Direct3D) - ソーサリーフォース

 モデルの描画・ライティング・カメラと一通り丁寧に解説されています。
 ただし今の自分の環境ではサンプルの実行ファイルが起動できません。
 また、提示されているコードを試してみるとフォームが表示されず。。

C#でツールを作る その1-Direct3Dの初期化- - while( c++ );

 順に更新記事を辿っていくと記事も多く色々と試されています。

3D でウインドウを表示

01 - DirectX + C# 3D basic Terrain Generator Tutorial (setting up DX plus 1 basic triangle) - YouTube 

SlimDXを使う方法


SlimDX - Wikipedia

 SlimDXC# で DirectX11 を使うためのラッパーだそうです。
 Microsoft公式のラッパーよりも使い易いとか。
 SlimDXの公式サイトが見れない状態ですが、Visual StudioからNuGetで検索してインストールすることで無事に動作しました。

C#でDirectX11入門一覧 (SlimDX入門)|Memeplexes

 SlimDXを使った3D表示についてかなり丁寧に解説されています。
 しかもカメラ・ライティング・テクスチャなど一通り取り上げられています。
 こちらをベースに勉強していくと良さそうです。
 掲載されているコード自体にはコメントを記載されていないので、とりあえずコードを移植したら、解説をじっくり読みつつ自分で該当箇所にコメントを入れていく感じで見ていっています。

C#でDirectX 軽い気持ちでSlimDXを使うとき用ライブラリSlimDXSketchを作りました - 渋谷ほととぎす通信

 ひとつ前のSlimDX入門の記事をアレンジして解説されています。
 こちらの「三角形を描画する」の記事では、「myEffect2.fx」という名前のファイルを追加して、記事中で解説されているシェーダのコードをこちらに記載し、実行ファイルと同じ場所に置いてやればOKです。

という訳でとりあえず三角形の描画ができました!

f:id:moko_03_25:20190108171148j:plain

 

OpenTKを使う方法


OpenTK(Open Toolkit Library)は C#OpenGL を使うためのラッパーだそうです。
こちらもNuGetからインストールするので良いようです。

Index | #region OpenTK

 OpenTKの使い方をかなり多岐に渡って解説されています。
 Windowsフォームに組み込む際に関しての解説もあります。

MonoGameを使う方法

 

MonoGame

 C#で記述できてDirectXを扱い易くしたというXNAは終了しましたが、マイクロソフト自身がXNAを移植したものがMonoGameだそうで、XNAのコードがほぼそのまま動くそうです。

 ちなみにこの記事を書いた2019.1.13 時点では、公式サイトでDLできる最新バージョンは 3.7.1 で、インストーラーにVisual Studio 2017のTemplateが含れていました。

MonoGameで2Dゲームを作る(前書き) - Qiita
MonoGame入門Ⅰ - Qiita
MonoGame で 3D 描画 - Qiita

 MonoGame自体の解説が少なそうですが、XNAのサンプルが活用できるようなのでそのあたりでカバーできて良いかも知れません。

かんたんXNA4.0 (XNA入門)|Memeplexes

 SlimDXの項目でもリンクを貼っていますが、こちらではXNAも豊富に解説されています。
 ただMonoGameだとすんなり画像が読めないなどあるのでこちらを参考にしてうまくいきました。

ActiVizを使う方法


Kitware - Experts in Visualization Technologies and Image Processing
VTK - The Visualization Toolkit

 ActiVizC# で VTK(The Visualization ToolKit)を使うためのラッパーだそうです。
 こちらもNuGetからインストールするので良いようです。

C#で3Dモデル表示(VTKのC#ラッパActiVizによる方法) - whoopsidaisies's diary



C# ハイトマップからノーマルマップを生成してみた

C#による画像処理の練習がてら、ハイトマップからノーマルマップの生成を試してみました。
プルダウンで選んでいる数字が高いほど法線の傾斜が緩くなっています。

f:id:moko_03_25:20190107013038g:plain

こちらをざっくりと解説してみます。

続きを読む

使用条件 / 免責事項

作者 / 著作権


moko

動作確認済みのOS


Windows7(64bit)

使用条件


商用・非商用に関わらず(プライベートでもお仕事でも)ご自由にお使いください。

配布条件


作者の許可無く、本ソフトウェアを複製・第三者に配布しないでください。

免責事項


本ソフトウェアは予告なく機能を変更することがあります。
本ソフトウェアは動作環境を満たす全ての環境で正常に動作することを期待していますが、保証はできません。
本ソフトウェアの使用により生じたいかなる損害に関して、作者は一切の責任を負いません。

問い合わせ先


不具合やご質問などのお問い合わせは、Twitterの作者のアカウントまたはメールアドレスまでお願いします。

https://twitter.com/moko_03_25
moko.vfx@gmail.com