simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Windows Subsystem for Linux(WSL)で Docker を利用する

前書き

Docker for Windows 派なのですが、仮想化環境だったとしても Hyper-V が対応していない PC だったりして、結局 Docker for Windows や WSL2 が起動しないなんてことになってます。
AWS などで、お安いインスタンスを借りることで Linux 環境は整いますが、ローカルでお金をかけずに気軽にコンテナを確認したい時だってあるじゃないですか
そんな感じで、WSL x Docker でローカルに http サーバーの nginx のコンテナを起動して動作を見てみようと思います。

手順

WSL機能を有効にする

管理者権限で PowerShell を起動して

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

GUI で有効無効に切り替えることもできます。

f:id:simplestar_tech:20191014093520p:plain
プログラムの追加と削除から

WSLのパッケージをストアから導入

Microsoft Store から WSL について調べると、対象ページに案内されます

f:id:simplestar_tech:20191014093839p:plain
Microsoft Store
ここで Ubuntu を選んでインストール

管理者権限で Ubuntu を起動

いろいろ初回は聞かれるので、今までの手順含め詳細を学びたい初心者はこちらを読むといいかも
www.atmarkit.co.jp

Docker を WSL Ubuntu にインストール & 起動する

要約すると

sudo apt update
sudo apt upgrade -y
sudo apt install -y docker.io

docker が新しすぎるとダメなので戻す(この情報になかなか辿り着けなかった

戻せるバージョンを確認し

sudo apt show docker.io -a

Docker をわざと古くする

sudo apt remove docker.io
sudo apt -y install docker.io=17.12.1-0ubuntu1

Docker を起動する

sudo cgroupfs-mount && sudo service docker start

起動確認とか

sudo docker version

Client:
 Version:       17.12.1-ce
 API version:   1.35
 Go version:    go1.10.1
 Git commit:    7390fc6
 Built: Wed Apr 18 01:23:11 2018
 OS/Arch:       linux/amd64

Server:
 Engine:
  Version:      17.12.1-ce
  API version:  1.35 (minimum version 1.12)
  Go version:   go1.10.1
  Git commit:   7390fc6
  Built:        Wed Feb 28 17:46:05 2018
  OS/Arch:      linux/amd64
  Experimental: false

docker コマンドは頻繁に打つのでいちいち sudo つけないといけないのうざい人は docker グループに User を追加して対処します。

# dockerグループがなければ作る
sudo groupadd docker

# 現行ユーザをdockerグループに所属させる
sudo gpasswd -a $USER docker

# docker 再起動
sudo service docker restart

# いったん Ubuntu も管理者権限で起動しなおし
exit

参考:
Dockerコマンドをsudoなしで実行する方法 - Qiita

起動しなおしたら、再度 docker を立ち上げ

sudo cgroupfs-mount && sudo service docker start

起動確認

docker run --rm hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

これができなくてずっと調べて、試行錯誤してました。
2019の台風19号が上陸する日の朝のことです

nginx とか

localhost:8080 で Web サーバーが起動するように nginx コンテナを pull して起動する

docker run --name hogehoge -d -p 8080:80 nginx
  • d は デタッチ の略、ようはフォアグラウンドで居座らないようにするオプション
  • p はコンテナの外にポートフォワーディングということで、ホストマシンの 8080 ポートが、nginx コンテナの 80 ポートに接続される意味

もちろんホスト側で 8080 を誰かが使っていると怒られる
あとは docker の基礎を学ぶとだいたいクリアできると思います

結果確認
localhost:8080 と Chrome などの Web ブラウザの URL に記入する

f:id:simplestar_tech:20191014101428p:plain
Web サーバーが docker コンテナ in WSLで動いてる証拠

まとめ

docker が動けばいいんだよ
あとは docker だけの知識で頑張るから

追記

WSL は /mnt というディレクトリがあり、ここに C ドライブ, D ドライブなど、パソコンのハードディスクドライブがマウントされています。(mnt だけに)

:/mnt$ ls -la
total 0
drwxrwxrwx 1 owner owner  512 Oct 13 08:12 c
drwxrwxrwx 1 owner owner  4096 Oct 12 13:21 d

Windows のファイルを使って docker コンテナ起動したいんじゃー、みたいな時は、この /mnt ディレクトリを活用できそう

Unity:UniversalRP de MasterNode (v7.1.2)

目まぐるしく変わる Unity の Universal RP の実装

その実装に合わせてカスタム MasterNode を作り替え続ける備忘録である

手順

Unity 2019.3.0b6 を落して、テンプレートに Universal RP を選ぼう

f:id:simplestar_tech:20191007213216p:plain
UniversalRPテンプレートを選ぶ

起動すると次の画面になりますね

f:id:simplestar_tech:20191007213954p:plain
Scene

Create Shader をチェック

f:id:simplestar_tech:20191007214101p:plain
ま、そうだな

Toon Graph 項目を増やす

Library/PackageCache フォルダを見ます

f:id:simplestar_tech:20191007214407p:plain
RP 関連

この三つのフォルダを Packages フォルダへ移動しておきます。

f:id:simplestar_tech:20191007214457p:plain
こんな風に

で PBR Graph で文字列検索して次の通り Toon Graph を作る項目を追加します。

f:id:simplestar_tech:20191007214601p:plain
Toon の項目を足します

いえー、増えた

f:id:simplestar_tech:20191007214923p:plain
Toon Graph ここにあり

ToonMasterNode クラス作ろう

次のファイルを作成して
"Packages\com.unity.shadergraph@7.1.2\Editor\Data\MasterNodes\IToonSubShader.cs"

このように実装します。

f:id:simplestar_tech:20191007215308p:plain
IToonSubShader

同じフォルダにある PBRMasterNode.cs ファイルを複製して ToonMasterNode.cs とし…
ファイル内の PBR を Toon に全部置換します

次のファイルを同じフォルダにある PBRSettingsView.cs を複製して作ります
Packages\com.unity.shadergraph@7.1.2\Editor\Drawing\Views\ToonSettingsView.cs
ファイル内の PBR を Toon に全部置換します

次のファイルも同様に、同じ階層の PBRMasterGUI.cs を複製して作ります。
Packages\com.unity.shadergraph@7.1.2\Editor\ShaderGUI\ToonMasterGUI.cs
ファイル内の PBR を Toon に全部置換します

これで、最初の UI メニュー項目の変更箇所を次のように ToonMasterNode に置き換え

        [MenuItem("Assets/Create/Shader/Toon Graph", false, 208)]
        public static void CreateToonMasterMaterialGraph()
        {
            GraphUtil.CreateNewGraph(new ToonMasterNode());
        }

これで壊れた Toon Master Node を Shader Graph で作れるようになりました

f:id:simplestar_tech:20191007220710p:plain
こわれた MasterNode が作れるようになった

UniversalPBRSubShader を複製する

次のファイルを複製
Packages\com.unity.render-pipelines.universal@7.1.2\Editor\ShaderGraph\SubShaders\UniversalPBRSubShader.cs
UniversalToonSubShader.csファイルにリネームして
ファイル内の PBR を Toon に全部置換します(小文字の pbr は残す)

次のパスのファイルを同じフォルダのPBRForwardPass.hlslを複製して作ります
Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/ToonForwardPass.hlsl
ファイル内の PBR を Toon に全部置換します(一か所だけ)

次のパスのファイルを同じフォルダのPBR2DPass.hlslを複製して作ります
Packages\com.unity.render-pipelines.universal@7.1.2\Editor\ShaderGraph\Includes\Toon2DPass.hlsl
ファイル内に PBR はないので置換は不要

次のファイル内を見ましょう
Packages\com.unity.render-pipelines.universal@7.1.2\ShaderLibrary\Lighting.hlsl

UniversalFragmentPBR 関数をコピーして UniversalFragmentToon 関数を次の通り追加します

half4 UniversalFragmentToon(InputData inputData, half3 albedo, half metallic, half3 specular,
	half smoothness, half occlusion, half3 emission, half alpha)
{
	BRDFData brdfData;
	InitializeBRDFData(albedo, metallic, specular, smoothness, alpha, brdfData);

	Light mainLight = GetMainLight(inputData.shadowCoord);
	MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, half4(0, 0, 0, 0));

	half3 color = GlobalIllumination(brdfData, inputData.bakedGI, occlusion, inputData.normalWS, inputData.viewDirectionWS);
	color += LightingPhysicallyBased(brdfData, mainLight, inputData.normalWS, inputData.viewDirectionWS);

#ifdef _ADDITIONAL_LIGHTS
	uint pixelLightCount = GetAdditionalLightsCount();
	for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
	{
		Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
		color += LightingPhysicallyBased(brdfData, light, inputData.normalWS, inputData.viewDirectionWS);
	}
#endif

#ifdef _ADDITIONAL_LIGHTS_VERTEX
	color += inputData.vertexLighting * brdfData.diffuse;
#endif

	color += emission;
	return half4(color, alpha);
}

Unity にフォーカスを与えると Shader ビルドが走るので、先ほどの壊れた Shader Graph が正常な Shader として扱われるようになります

f:id:simplestar_tech:20191007222827p:plain
いえー!!

おまけ

Lighting.hlsl の UniversalFragmentToon を次の通り emissionを消してみました

half4 UniversalFragmentToon(InputData inputData, half3 albedo, half metallic, half3 specular,
	half smoothness, half occlusion, half3 emission, half alpha)
{
	BRDFData brdfData;
	InitializeBRDFData(albedo, metallic, specular, smoothness, alpha, brdfData);

	Light mainLight = GetMainLight(inputData.shadowCoord);
	MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, half4(0, 0, 0, 0));

	half3 color = GlobalIllumination(brdfData, inputData.bakedGI, occlusion, inputData.normalWS, inputData.viewDirectionWS);
	color += LightingPhysicallyBased(brdfData, mainLight, inputData.normalWS, inputData.viewDirectionWS);

	uint pixelLightCount = GetAdditionalLightsCount();
	for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
	{
		Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
		color += LightingPhysicallyBased(brdfData, light, inputData.normalWS, inputData.viewDirectionWS);
	}

	color += inputData.vertexLighting * brdfData.diffuse;

	return half4(color, alpha);
}

f:id:simplestar_tech:20191007223452p:plain
emissionガン無視

まとめ

PBR シェーダを複製していき Toon MasterNode を作って Shader Graph 経由で作成、編集できることを示しました。
頑張って Shader の実装を書き換えていけば Toon の Master Node が作れるってわけです。

ここまでの知識を次の知見につなげれば、なんとかなりそう…
simplestar-tech.hatenablog.com

v7.1.2 でも動く Toon Shader Graph の完成に期待(未来の自分頑張れ)

Unity:CubeWalkゲームのキューブの破壊

前作のキューブの破壊は?

f:id:simplestar_tech:20191005152608p:plain
今回の更新で洞窟やかまくらが作れるようになりました

昔はこんな感じでキューブの位置から、キューブのデータを特定していた模様

    internal unsafe bool GetCubeByPosition(Vector3 position, out Vector3Int chunkIndex, out Vector3Int cubeIndex, out CubeSt cube)
    {
        chunkIndex = new Vector3Int(
            Mathf.FloorToInt(position.x / (ChunkSizeX * CubeSide)),
            Mathf.FloorToInt(position.y / (ChunkSizeY * CubeSide)),
            Mathf.FloorToInt(position.z / (ChunkSizeZ * CubeSide)));

        var origin = _chunkTensor.GetFromOrigin(Vector3Int.zero);
        if (null != origin.value)
        {
            var originChunkIndex = origin.value.chunkIndex;

            var chunkObject = _chunkTensor.GetFromOrigin(new Vector3Int(chunkIndex.x - originChunkIndex.x, chunkIndex.y - originChunkIndex.y, chunkIndex.z - originChunkIndex.z)).value;
            if (null != chunkObject)
            {
                var entity = chunkObject.gameObject.GetComponent<GameObjectEntity>().Entity;
                var em = World.Active.GetExistingManager<EntityManager>();
                ChunkData data = em.GetComponentData<ChunkData>(entity);
                int x = (int)(position.x / CubeSide) % ChunkSizeX;
                x = 0 > x ? ChunkSizeX + x : x;
                int y = (int)(position.y / CubeSide) % ChunkSizeY;
                y = 0 > y ? ChunkSizeY + y : y;
                int z = (int)(position.z / CubeSide) % ChunkSizeZ;
                z = 0 > z ? ChunkSizeZ + z : z;

                cube = new CubeSt();
                cubeIndex = new Vector3Int(x, y, z);
                int ct = (x * ChunkSizeZ * ChunkSizeY + z * ChunkSizeY + y) * CubeSt.size;
                cube.cat = (CubeCategory)data.data[ct + 0];
                cube.rot = (CubeRotation)data.data[ct + 1];
                cube.sideA = (SideType)data.data[ct + 2];
                cube.sideB = (SideType)data.data[ct + 3];

                return true;
            }
        }

        cube = new CubeSt();
        cubeIndex = Vector3Int.zero;
        return false;
    }

chunkTensor? 知らない子ですね(ひどい!頑張ったのに無かったことにされてしまった)

空間座標からチャンクのインデックスまで求めるのは良さそうですね。
あとはチャンク内でのインデックスがわかればよし

前回の UI 操作で、マウスの左クリックイベントを発行して、ハンドラで座標を割り出しましょう。
ここでひらめくのは、対象オブジェクトがわかるわけで、それはチャンクなわけで…

あ!先にもこもこやります。

もこもことは?

カーソルが触れたキューブがゆっくりと大きくなり、そしてカーソルが外れると、ゆっくりと小さくなるキューブが
必要な時に現れて、不要な時に消えることを指します。

カーソルはこんな感じで、辞書から位置を使ってアクセスできるようにし

Dictionary<Vector3Int, GameObject> mocomocoCubes = new Dictionary<Vector3Int, GameObject>();

カーソルの先に触れたキューブとして、なければ Instantiate, あればそれを表示

こ、これだと無限に居続けるので、3秒したら辞書から自身を消して Destroy するようにしてみます。(+現れたときに大きくなり始め、消えるまで小さくなり続ける)

想像していたのと違う…

カーソルが当たっている間は大きいままでいてほしく、カーソルが外れたら小さくなりはじめてほしい

こんな感じですね。
キューブの Prefab に割り当てたコンポーネントがこちら(毎回思うけど、4次元アニメーションを1次元コードに落とし込むこの作業はな…集中力をごっそりもってかれる)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 大きくなって、元のサイズに戻った時に消える
/// </summary>
public class SelectedCubeAnimator : MonoBehaviour
{
    #region Params
    [SerializeField] float selectedScale = 1.5f;
    [SerializeField] float scaleSpeed = 0.01f;
    #endregion

    public void ScaleStart()
    {
        this.mocomocoCubes = null;
        if (null != this.coroutine)
        {
            StopCoroutine(this.coroutine);
        }
        coroutine = StartCoroutine(this.CoScaleThis());
    }

    public void ScaleEnd(Dictionary<Vector3Int, GameObject> mocomocoCubes)
    {
        this.mocomocoCubes = mocomocoCubes;
        if (null != this.coroutine)
        {
            StopCoroutine(this.coroutine);
        }
        coroutine = StartCoroutine(this.CoDestroyThis());
    }

    IEnumerator CoScaleThis()
    {
        while (this.selectedScale > this.transform.localScale.x)
        {
            this.transform.localScale = Vector3.one * (this.transform.localScale.x + this.scaleSpeed);
            yield return null;
        }
    }

    IEnumerator CoDestroyThis()
    {
        while (1 < this.transform.localScale.x)
        {
            this.transform.localScale = Vector3.one * (this.transform.localScale.x - this.scaleSpeed);
            yield return null;
        }
        if (null != this.mocomocoCubes)
        {
            this.mocomocoCubes.Remove(new Vector3Int((int)this.transform.position.x, (int)this.transform.position.y, (int)this.transform.position.z));
            Destroy(this.gameObject);
        }
    }

    Coroutine coroutine;
    Dictionary<Vector3Int, GameObject> mocomocoCubes;
}

キューブデータの特定

触れている Chunk オブジェクトは RayCast の戻り値で得られているはずなので、そこからチャンクのインデックスを求めてみます。

いい感じですね。
実装は次の通り変化しました。(MeshSystemの修正を頑張った…Mesh API v2 使ってるんですよ、新しいです)

using CubeWalk;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// カーソルが当たったキューブの位置にアニメーションするキューブを配置
/// </summary>
public class CubeSelector : MonoBehaviour
{
    #region SceneComponents
    [SerializeField] InputModeSwitcher inputModeSwitcher;
    [SerializeField] GameObject cursorCube;
    [SerializeField] CubeDataWorld cubeDataWorld;
    [SerializeField] ChunkMeshBehaviour chunkMeshBehaviour;
    #endregion

    void Start()
    {
        this.inputModeSwitcher.onChangeInputMode += OnChangeInputMode;
    }

    private void OnChangeInputMode(bool showCursor)
    {
        // カーソルが表示されているフラグを立てる
        this.existingCursor = showCursor;
        if (!showCursor && this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube))
        {
            // 以前のキューブを削除
            var animator = cube.GetComponent<SelectedCubeAnimator>();
            animator.ScaleEnd(this.mocomocoCubes);
            this.cubeSelected = false;
        }
    }

    void Update()
    {
        if (!this.existingCursor)
            return;
        // スクリーン中のマウスポインタに向かってカメラからレイを作成
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        LocateCursorCube(ray);
    }

    private void LocateCursorCube(Ray ray)
    {
        // レイが CubeChunk にヒットした場合にキューブの位置を特定
        if (this.cubeSelected = Physics.Raycast(ray, out RaycastHit cameraHit, IntaractRayLength, LayerMask.GetMask(LayerMask_Level)))
        {
            Vector3 cubePosition = cameraHit.point - (cameraHit.normal * ChunkConst.CubeSide * 0.5f);

            Vector3 roundSource = cubePosition / ChunkConst.CubeSide;
            Vector3Int gridCubePosition = new Vector3Int(Mathf.RoundToInt(roundSource.x), Mathf.RoundToInt(roundSource.y), Mathf.RoundToInt(roundSource.z));
            Vector3 newCubePositon = new Vector3(gridCubePosition.x, gridCubePosition.y, gridCubePosition.z) * ChunkConst.CubeSide;

            // 現在配置しているカーソルキューブの位置からずれたか確認
            if (0 == this.mocomocoCubes.Count || this.currentCubePosition != gridCubePosition)
            {
                if (this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube))
                {
                    // 前のキューブを削除
                    var animator = cube.GetComponent<SelectedCubeAnimator>();
                    animator.ScaleEnd(this.mocomocoCubes);
                }

                // カーソルキューブを再配置
                if (!this.mocomocoCubes.TryGetValue(gridCubePosition, out cube))
                {
                    // 辞書になければ追加
                    cube = Instantiate(this.cursorCube, this.transform);

                    #region マテリアルの設定
                    if (!chunkMeshBehaviour.materials.ContainsKey(chunkMeshBehaviour.meshShader.GetInstanceID()))
                    {
                        var material = new Material(chunkMeshBehaviour.meshShader);
                        material.SetTexture("Texture2D_5418CE01", chunkMeshBehaviour.textureAlbedo);
                        chunkMeshBehaviour.materials.Add(chunkMeshBehaviour.meshShader.GetInstanceID(), material);
                    }
                    var renderer = cube.GetComponent<MeshRenderer>();
                    cube.GetComponent<MeshRenderer>().sharedMaterial = chunkMeshBehaviour.materials[chunkMeshBehaviour.meshShader.GetInstanceID()];
                    #endregion

                    // キューブデータの取得
                    this.cubeDataWorld.ChunkInt3ToChunkKey(cameraHit.collider.gameObject.GetComponent<ChunkMeshInfo>().chunkInt3, out Vector3Int chunkKeyXYZ);
                    unsafe
                    {
                        var pData = this.cubeDataWorld.CubeDataFromPosition(newCubePositon, chunkKeyXYZ);

                        var rotationType = (CubeRotationType)pData[(int)ChunkDataType.Rotation];
                        int row = (byte)rotationType % 17; // @debug
                        int col = (byte)rotationType / 17;

                        var vertexCount = 48; // cube verts
                        var nativeVertices = new NativeArray<ChunkMeshSystem.CustomVertexLayout>(vertexCount, Allocator.Temp);
                        var meshData = new ChunkMeshData { vertexCount = vertexCount, pVertices = (float*)nativeVertices.GetUnsafePtr() };
                        ChunkMeshSystem.FillCubeVertices48(pData, ref meshData);

                        // メッシュ作成
                        var mesh = new Mesh();
                        mesh.Clear();
                        // メッシュレイアウトを定義
                        var layout = new[]
                        {
                            new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
                            new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
                        };

                        mesh.SetVertexBufferParams(vertexCount, layout);
                        mesh.SetIndexBufferParams(vertexCount, IndexFormat.UInt32);

                        // インデックスを作成
                        var nativeIndices = new NativeArray<int>(vertexCount, Allocator.Temp);
                        for (int index = 0; index < vertexCount; index++)
                        {
                            nativeIndices[index] = index;
                        }

                        // 頂点データとインデックスを設定
                        mesh.SetVertexBufferData(nativeVertices, 0, 0, vertexCount);
                        mesh.SetIndexBufferData(nativeIndices, 0, 0, vertexCount);

                        // ネイティブ配列を開放
                        nativeVertices.Dispose();
                        nativeIndices.Dispose();

                        // サブメッシュの定義
                        mesh.subMeshCount = 1;
                        mesh.SetSubMesh(0, new SubMeshDescriptor
                        {
                            topology = MeshTopology.Triangles,
                            vertexCount = vertexCount,
                            indexCount = vertexCount,
                            baseVertex = 0,
                            firstVertex = 0,
                            indexStart = 0
                        });

                        // 法線と接戦とバウンディングボックスを作成
                        mesh.RecalculateNormals();
                        mesh.RecalculateTangents();
                        mesh.RecalculateBounds();

                        // メッシュの設定
                        var meshFilter = cube.GetComponent<MeshFilter>();
                        if (null != meshFilter.sharedMesh)
                        {
                            meshFilter.sharedMesh.Clear();
                        }
                        meshFilter.sharedMesh = mesh;
                    }

                    this.mocomocoCubes.Add(gridCubePosition, cube);
                    var animator = cube.GetComponent<SelectedCubeAnimator>();
                    animator.ScaleStart();
                }
                else
                {
                    // 既存のキューブを復活
                    var animator = cube.GetComponent<SelectedCubeAnimator>();
                    animator.ScaleStart();
                }
                cube.transform.position = newCubePositon;
                this.currentCubePosition = gridCubePosition;
            }
        }
        else if (this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube))
        {
            // 以前のキューブを削除
            var animator = cube.GetComponent<SelectedCubeAnimator>();
            animator.ScaleEnd(this.mocomocoCubes);
        }
    }

    Dictionary<Vector3Int, GameObject> mocomocoCubes = new Dictionary<Vector3Int, GameObject>();
    bool cubeSelected = false;
    Vector3Int currentCubePosition = Vector3Int.zero;
    bool existingCursor = false;
    readonly float IntaractRayLength = 20;
    readonly string LayerMask_Level = "CubeChunk";
}

マウスの左クリックイベントを発行

誰が?で悩みますが、CubeSelector ですかね。
誰に?はつくらないといけないですね。

キューブの破壊時の様子をイメージしていますが、クリックしたらいきなり破壊はなくて…
キューブの位置、情報、もろもろを受け取りつつ
クリック開始からクリック終了までのステータスを検知し続けて、
破壊については、破壊できない条件なども考えると、プレイヤーがカーソルに込めた道具の要素も一緒に受け取りたいですね。

そんなキューブ破壊を担当するのは Cube Destroyer さん
同じく、キューブの再配置は、左クリック開始と終了ステータスを受け取り、配置前に回転を制御できたりと
そんなキューブを再配置してくれるのが Cube Locator さん

CubeSelector が各種イベントをパラメータ付きで発行して、Destroyer と Locator がこれを購読する枠組みだけ先に用意しちゃいます。

破壊前は、VFX Graph でそのキューブの小さい欠片が舞って、火花とか散って、最後に爆発してほしいなと。。。未来の自分さんに注文つけとこう

Cube Destroyer さん

マウスクリックステートと、キューブを選択していること、キューブのメッシュオブジェクトと、カーソルが触れている位置を取得できています。
VFXEditor で、キューブがわーっとある一点から吹き出してくるもの作れませんか?

そして、一通り Visual Effect Editor の構文を学んだのですが、キューブの世界との干渉は行えず(GPU処理のため)
とくにパフォーマンスは求めなかったので Shuriken を使うことにしました。

そうして注文を受けて、形作られた CubeDestroyer 実装がこちら

using CubeWalk;
using UnityEngine;

/// <summary>
/// キューブの破壊処理
/// </summary>
public class CubeDestroyer : MonoBehaviour
{
    #region Scene Components
    [SerializeField] CubeDataWorld cubeDataWorld;
    [SerializeField] CubeSelector cubeSelector;
    [SerializeField] VRMPlayerModelLoader vrmCharacters;
    #endregion

    #region Assets
    [SerializeField] GameObject miningEffectPrefab;
    [SerializeField] GameObject explosionEffectPrefab;
    #endregion

    void Start()
    {
        // キューブ選択処理のイベントを購読
        this.cubeSelector.onDestroyStarted += this.OnDestroyStarted;
        this.cubeSelector.onDestroyUpdated += this.OnDestroyUpdated;
        this.cubeSelector.onDestroyCanceled += this.OnDestroyCanceled;
        this.cubeSelector.onChangeSelected += this.OnChangeSelected;
    }

    /// <summary>
    /// 破壊開始
    /// </summary>
    void OnDestroyStarted(RaycastHit raycastHit, GameObject cubeObject)
    {
        // カリカリというか、そういう掘削時に発生するエフェクトが出現(寿命が来たら自動で消えるようにスケジュール)
        this.miningEffect = Instantiate(this.miningEffectPrefab, raycastHit.point, Quaternion.LookRotation(raycastHit.normal), this.transform);
        var particleSystem = this.miningEffect.GetComponent<ParticleSystem>();
        Destroy(this.miningEffect, particleSystem.main.duration + particleSystem.main.startLifetime.constant);
        // エフェクトが利用するレンダラーに対象のキューブメッシュ情報を渡す
        var renderer = particleSystem.GetComponent<ParticleSystemRenderer>();
        renderer.mesh = cubeObject.GetComponent<MeshFilter>().sharedMesh;
        renderer.sharedMaterials = cubeObject.GetComponent<MeshRenderer>().sharedMaterials;
        particleSystem.Play();

        // キューブ選択スケールを解除
        var cubeAnimator = cubeObject.GetComponent<SelectedCubeAnimator>();
        cubeAnimator.ScaleEnd(null);

        // 破壊中 or 破壊時に必要なステート情報を保持
        this.chunkCollider = raycastHit.collider;
        this.cubeObject = cubeObject;
        this.cubeCenter = cubeObject.transform.position;
        this.cubeLife = 1.0f;
    }

    /// <summary>
    /// 破壊停止
    /// </summary>
    void OnDestroyCanceled()
    {
        CancelSchedule();
        // キューブ選択スケールを再開
        if (null != this.cubeObject)
        {
            var cubeAnimator = this.cubeObject.GetComponent<SelectedCubeAnimator>();
            cubeAnimator.ScaleStart();
        }
    }

    private void CancelSchedule()
    {
        // 掘削エフェクト中だった場合は止める
        if (null != this.miningEffect)
        {
            var particleSystem = this.miningEffect.GetComponent<ParticleSystem>();
            particleSystem.Stop();
        }
        this.cubeLife = 1.0f;
    }

    /// <summary>
    /// 対象キューブが変わった
    /// </summary>
    private void OnChangeSelected()
    {
        // 破壊停止と同様にスケジュールを中止
        this.CancelSchedule();
    }

    /// <summary>
    /// 掘削中
    /// </summary>
    void OnDestroyUpdated(RaycastHit raycastHit, GameObject cubeObject)
    {
        // 掘削エフェクトがあれば位置と向きを調整
        if (null != this.miningEffect)
        {
            this.miningEffect.transform.position = raycastHit.point;
            this.miningEffect.transform.rotation = Quaternion.LookRotation(raycastHit.normal);
        }
        this.cubeLife -= 0.01f; // @debug 掘削時間はツールとキューブによって変化させる予定
        if (0 > this.cubeLife)
        {
            this.cubeLife = 100.0f; // 連続で破壊が行われないようにフタをする
            // 破壊
            this.ExplodeCube();
        }
    }

    /// <summary>
    /// 破壊
    /// </summary>
    void ExplodeCube()
    {
        // カーソルキューブを一瞬で非表示
        var cursorRenderer = this.cubeObject.GetComponent<Renderer>();
        cursorRenderer.enabled = false;

        // 破壊エフェクトを開始&破棄をスケジュール
        var explosionEffect = Instantiate(this.explosionEffectPrefab, this.cubeCenter, this.miningEffect.transform.rotation, this.transform);
        var explosionParticleSystem = explosionEffect.GetComponent<ParticleSystem>();
        Destroy(explosionEffect, explosionParticleSystem.main.duration);
        var miningParticleSystem = this.miningEffect.GetComponent<ParticleSystem>();
        var renderer = explosionParticleSystem.GetComponent<ParticleSystemRenderer>();
        var miningRenderer = miningParticleSystem.GetComponent<ParticleSystemRenderer>();
        renderer.mesh = miningRenderer.mesh;
        renderer.sharedMaterials = miningRenderer.sharedMaterials;
        explosionParticleSystem.Play();

        // バキューム対象をセット
        var vInput = this.vrmCharacters.GetComponentInChildren<Invector.vCharacterController.vThirdPersonInput>();
        if (null != vInput)
        {
            var animator = explosionEffect.GetComponent<ExploadedCubeAnimator>();
            animator.SetVacuumeOrigin(vInput.transform);            
        }

        // 対象キューブを空気に
        var chunkInt3 = this.chunkCollider.gameObject.GetComponent<ChunkMeshInfo>().chunkInt3;
        this.cubeDataWorld.SetCubeData(this.cubeCenter, chunkInt3, CubeCategoryType.Basic, CubeRotationType.Top000, SideType.Air, SideType.Air);
    }

    float cubeLife = 1.0f;
    GameObject cubeObject = null;
    GameObject miningEffect = null;
    Vector3 cubeCenter = Vector3.zero;
    Collider chunkCollider = null;
}

破壊するキューブが隣接するチャンクの特定

キューブを破壊する Mesh 更新処理にも一工夫必要で…
チャンクに隣接している場合はそのチャンクのメッシュも更新しないと世界に穴が空いてしまいます。
そこは過去の経験でこのようにカバー(汚いが、3次元を展開して1次元に書き表すとはこのこと)

        void OnUpdateChunkData(Vector3Int chunkInt3, Vector3Int cubeInt3, uint loadTaskGeneratoin)
        {
            // もし cube が上のチャンクと接していたら、上のチャンクも更新対象とする
            if ((ChunkConst.ChunkSizeY - 1) == cubeInt3.y)
            {
                var nextChunkInt3 = chunkInt3 + Vector3Int.up;
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }
            // もし cube が下のチャンクと接していたら、上のチャンクも更新対象とする
            if (0 == cubeInt3.y)
            {
                var nextChunkInt3 = chunkInt3 + Vector3Int.down;
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }

            // もし cube が右のチャンクと接していたら、右のチャンクも更新対象とする
            if ((ChunkConst.ChunkSizeX - 1) == cubeInt3.x)
            {
                var nextChunkInt3 = chunkInt3 + Vector3Int.right;
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }
            // もし cube が左のチャンクと接していたら、左のチャンクも更新対象とする
            if (0 == cubeInt3.x)
            {
                var nextChunkInt3 = chunkInt3 + Vector3Int.left;
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }

            // もし cube が奥のチャンクと接していたら、奥のチャンクも更新対象とする
            if ((ChunkConst.ChunkSizeZ - 1) == cubeInt3.z)
            {
                var nextChunkInt3 = chunkInt3 + new Vector3Int(0, 0, 1);
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }
            // もし cube が左のチャンクと接していたら、左のチャンクも更新対象とする
            if (0 == cubeInt3.z)
            {
                var nextChunkInt3 = chunkInt3 + new Vector3Int(0, 0, -1);
                if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3))
                {
                    var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                    this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo);
                }
            }

            // 対象チャンク
            if (!this.immediateUpdateEntityMap.ContainsKey(chunkInt3))
            {
                var createChunkInfo = new CreateChunkInfo { chunkInt3 = chunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin };
                this.immediateUpdateEntityMap.Add(chunkInt3, createChunkInfo);
            }
        }

キューブの破壊の適用結果

上記の実装を経て、やっと心象風景の具現化ができてきました。
こちらが、「キューブの破壊」のアウトプットです。

ビデオゲームのたのしさのデザイン

UniteTokyo2019 に参加してきました。

そこで休憩時間に流れた動画を見ていて、気になって一通り見ようと思ったので、次の動画を見ることにしました。
あそびのデザイン講座 特別編「たのしさの作り方」

学んだこととして、あそびの4要素

競争 めまい 偶然 模倣
それらをまとめる ハイ・コンセプトがあると良く

世界、キャラ、移動、敵、アクション、アイテム、エフェクトが揃っても…
すぐに見飽きてつまらなくなる

ゲームの楽しさは設計されている

人の習性を利用しよう

1.動くものに注意を払う
2.人は分類、整頓してユニークを見つけだす
3.環境の制限に緊張し、撤廃するよう行動する
4.音で状況を理解しようとする

これらを利用すれば、デザインでプレイヤーを次のアクションへ誘導できる

状況の把握、習性による欲求→計画→実践→結果

計画=結果

となったとき「たのしい!」が生まれる

実践→結果

イテレーションが高いと、成功するまでやっきになる

モチベーションが無くなるとつらい作業になってしまう

緊張したところに安心できるゴールを見せる
それを妨げる障害を認知させ、計画させる

ゴールした時に、ご褒美を与える

成功体験は脳に不可逆の現象を生み、忘れない
もとのからだに戻れなくなり、次がないと飽きる

というもの

マズローの心理学が出てきましたね。

そういえば昔、記事に引用したなぁと
現在のキューブの世界を作ろうとしたきっかけが見える記事でした
simplestar-tech.hatenablog.com

CubeWalk:キューブの選択

まえがき

2度の作り直しを経て、現在は VRoid Hub SDK 経由で VRM キャラクターデータを利用して Cube の世界を自由に歩き回ることができるようになっています。
www.youtube.com

バックエンドサービスの PlayFab の CloudScript から API Gateway と Lambda 経由で AWS の ElastiCache のデータを読み書きできるようになったので
再び Unity クライアントサイドで、キューブの選択と破壊、設置を行える仕組みを入れていきます。

前試作映像

参考までに前回、実装していた内容を確認
www.youtube.com

データ自体はすべてクライアント側で処理していたので、不正し放題でしたが、これからはクライアントまでマスターデータは降りてこず
世界中のプレイヤーとキューブのデータをサーバー上でやりとりすることになります。

大きな違いは、前作は FPS (主人公視点)だったのに対し、今作は自身のアバターを確認しながら進められるようTPS(三人称視点)で操作が進みます。
一人称視点は画面中央にカーソルを置けば、視点操作のみでブロックを選択できましたが、三人称視点はそうはいきません。

新しい UI

ゲーム内にて、カーソルを表示し、見えている範囲でカーソルで選択したキューブに対してアクションができるようにしてみようと思います。
いずれ FPS をサポートしたときも、同様にカーソルを使って視点を動かすことなくキューブへのアクションができるようになります。

Ctrl キーでカメラロックしたときに、ゲーム内カーソルが出現し、カーソルの先にあるキューブが選択できるようにしてみます。

simplestar-tech.hatenablog.com

でーきたっと…

選択されたことの合図

前作では透明で少し大きめのキューブを明滅させていましたが、今回は対象となるキューブが「もこ」っとアニメーションしながら膨れる
という変化で対応してみようと思います。

カーソルが当たっているキューブをどのように特定するか

選択ロジックをみてみましょう

using CubeWalk;
using UnityEngine;

public class CubeSelector : MonoBehaviour
{
    #region SceneComponents
    [SerializeField] GameInputMode gameInputMode;
    [SerializeField] GameObject cursorCube;
    #endregion

    void Start()
    {
        this.gameInputMode.onChangeInputMode += OnChangeInputMode;
    }

    private void OnChangeInputMode(bool showCursor)
    {
        // カーソルが表示されているフラグを立てる
        this.existingCursor = showCursor;
        // カーソル表示中はカーソルキューブも表示する
        this.cursorCube.GetComponent<Renderer>().enabled = showCursor;
    }

    void Update()
    {
        if (!this.existingCursor)
            return;
        // スクリーン中のマウスポインタに向かってカメラからレイを作成
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        LocateCursorCube(ray);
    }

    private void LocateCursorCube(Ray ray)
    {
        // レイが CubeChunk にヒットした場合にキューブの位置を特定
        if (Physics.Raycast(ray, out RaycastHit cameraHit, IntaractRayLength, LayerMask.GetMask(LayerMask_Level)))
        {
            Vector3 cubePosition = cameraHit.point - (cameraHit.normal * ChunkConst.CubeSide * 0.5f);

            Vector3 roundSource = cubePosition / ChunkConst.CubeSide;
            Vector3Int gridSource = new Vector3Int(Mathf.RoundToInt(roundSource.x), Mathf.RoundToInt(roundSource.y), Mathf.RoundToInt(roundSource.z));
            Vector3 newCubePositon = new Vector3(gridSource.x, gridSource.y, gridSource.z) * ChunkConst.CubeSide;

            // 現在配置しているカーソルキューブの位置からずれたか確認
            if ((this.currentCubePosition - newCubePositon).magnitude >= ChunkConst.CubeSide)
            {
                this.currentCubePosition = newCubePositon;

                // キューブデータの取得

                // キューブを再配置
                this.cursorCube.transform.position = newCubePositon;
            }
        }
    }

    Vector3 currentCubePosition = Vector3.zero;
    bool existingCursor = false;
    readonly float IntaractRayLength = 20;
    readonly string LayerMask_Level = "CubeChunk";
}

なるほど、法線とヒットした位置について丸めて、キューブの int 位置というものを特定しているようですね


カーソルが当たっているキューブに少し大きめのキューブを描画するテストを行ってみましょう。

チャンクの Layer を変更すると キャラクターが接地しない不具合を見つける
例の TPS アセットのキャラクターコントローラーの Layers プロパティを確認すると Default の Layer を利用することになっています。
グラウンドについては CubeChunk のレイヤーも見るように設定を加えました。(VRMにコントローラを割り当てるところ)

具体的にはここかな?

            var vController = vrmRoot.AddComponent<vThirdPersonController>();
            vController.groundLayer.value |= LayerMask.GetMask("CubeChunk");

正解

新しいキューブの選択デモ

上記の実装を走らせると次の通り
youtu.be

まとめ

Unity クライアントサイドで、キューブの選択と破壊、設置を行える仕組みを入れていきたいとして
今回はキューブの選択を UI どうしようか想像しながら、一つの案を具現化してみました。

いずれキューブの情報を参照
キューブの破壊
キューブの配置
とステップを踏んで、オンラインデータを更新、参照する仕組みにつなげていこうおと思います。

今回はここまで

Unity:CubeWalkゲームにてCtrl キーを押すとカーソルが現れる

まえがき

Unity で三人称視点でキャラクターを動かすアセットを利用しています。
assetstore.unity.com
このアセットをアクティブにするとマウスカーソルが消えてしまいます。

f:id:simplestar_tech:20190923120414p:plain
キャラクターがロードされるとマウスカーソルが消える(ロックされる)

変えたいこととしては、Ctrl キーを押した時にマウスカーソルが現れ、カメラの回転が固定される
また、できればそのカーソルはゲーム内であればカスタマイズされたカーソルであってほしい
Ctrl キーをもう一度押したらカーソルは消え、カメラの回転が有効になる
カーソルがアクティブになっていることは、そのロジックを司るコンポーネントからイベントで知ることができる

今の所、上記の機能は一つも実現できていないので、記事の最後にこれらが実現されたことをもって完了としたいと思います。

新しいコンポーネント

ゲームのロジックを司るコンポーネントになります。

シーン内でアクティブとなっているゲームのロジックはすべてヒエラルキーの GameManager オブジェクトの下にぶら下がっており
大枠で
PlayFab
World
Character
に分類されています。

ゲーム開始直後に PlayFab のユーザーとしてログインするのが PlayFab の処理
Cube の世界がプレイヤーを中心に広がっていく現象を司るのが World の処理
VRoidHub からキャラクターデータを作成して、プレイヤーを動作させることができるのが Character の処理
に分類されます。

ゲームに関するユーザー入力、モードの切り替わりなどは新しく Game の処理として
GameInputMode としてコンポーネントを切ってみます

ボタン押下の取得

Unity におけるキー押下の取得は InputSystem に移行します。
Input System 1.0.0 を Package Manager 経由でインストールしていくつかサンプルを確認します。
一番直感的なのは InputAction をフィールドに、インスペクタでボタン配置を指定するやり方

次の通り、Left Ctrl キーを押したことを検知するハンドラ登録をして、インスペクタで Ctrl であることを明示します。

時間は経っていますが、おおむね半年前と使い方は変わっていない模様
simplestar-tech.hatenablog.com

using UnityEngine;
using UnityEngine.InputSystem;

public class GameInputMode : MonoBehaviour
{
    public InputAction miningAction;

    private void Awake()
    {
        miningAction.performed +=
            ctx =>
            {
                this.miningMode = !this.miningMode;
                Debug.Log($"this.miningMode = {this.miningMode}");
            };
    }

    public void OnEnable()
    {
        miningAction.Enable();
    }

    public void OnDisable()
    {
        miningAction.Disable();
    }

    void Start()
    {
        
    }

    void Update()
    {
        
    }

    bool miningMode = false;
}

インスペクタでは + ボタンを押して Binding を選び Keyboard の Left Ctrl を選びます。
動作は問題ない

カーソルのロックの犯人探しとロック解除

三人称視点でキャラクターを動かすアセットがどこで何をしているのか確認してみます。

以下の通り VRM キャラクターに割り当たる vThirdPersonInput を見つけ出して、ロック関数を呼ぶと良い模様

var vInput = vrmCharacters.GetComponentInChildren<vThirdPersonInput>();
// カーソル再表示とカーソルロック解除
vInput.ShowCursor(vInput.lockInput);
vInput.LockCursor(vInput.lockInput);

ボタン押下時にためしたところ期待通り…動きました。

カメラ回転の停止

カメラの制御は CinemachineFreeLook の Axis Control にまかせているので、ここで Mouse Y, Mouse X を指定しているのでカメラが回転します。

一時的に Input Axis Name を空にすることで動かなくなるはずです。
ランタイムで切り替えられるかだけ確認します。

    /// <summary>
    /// Free Look カメラのロック
    /// </summary>
    /// <param name="lockFreeLook">ロックするときは true </param>
    void LockFreeLook(bool lockFreeLook)
    {
        var axisNameX = "";
        var axisNameY = "";
        if (lockFreeLook)
        {
            this.cinemachineFreeLook.m_XAxis.m_InputAxisValue = 0;
            this.cinemachineFreeLook.m_YAxis.m_InputAxisValue = 0;
        }
        else
        { 
            if (null != Gamepad.current)
            {
                axisNameX = "RightAnalogHorizontal";
                axisNameY = "RightAnalogVertical";
            }
            else
            {
                axisNameX = "Mouse X";
                axisNameY = "Mouse Y";
            }
        }
        this.cinemachineFreeLook.m_XAxis.m_InputAxisName = axisNameX;
        this.cinemachineFreeLook.m_YAxis.m_InputAxisName = axisNameY;
    }

問題なくカメラ制御を切り替えることができました。

カーソルをゲーム用に置き換える

これにはCursor.SetCursorを使います。情報ソースはこちら
kan-kikuchi.hatenablog.com

動きます。
テストに使うカーソルとして、こちらのフリーアセットを使ってみました。
assetstore.unity.com

カーソルのサイズを変更したいので Texture2D を任意のサイズにリサイズします。
リサイズには
TextureScale - Unify Community Wiki
のクラスを利用できました。

最終的に落ち着いたカーソル置換のコードがこちら

    void SetCursorImage(Texture2D cursorTexture)
    {
        // TextureScale.Bilinear(cursorTexture, 16, 16);
        Vector2 hotspot = new Vector2(0.293f, 0.078f) * 64;
        // カーソルの画像を Texture に設定
        Cursor.SetCursor(cursorTexture, hotspot, CursorMode.ForceSoftware);
    }

入力モード切り替えのイベント

UnityAction を受け付けるようにします。

    internal UnityAction<bool> OnChangeInputMode;

    public InputAction miningAction;

    private void Awake()
    {
        miningAction.performed +=
            ctx =>
            {
                this.miningMode = !this.miningMode;
                this.LockUnlockCursor(this.miningMode);
                this.LockFreeLook(this.miningMode);
                this.OnChangeInputMode?.Invoke(this.miningMode);
            };
    }

これで外部から入力がどのように切り替わったか確認、処理できるようになります。
カーソル変更はその先にしたいので、このコンポーネントからカーソルは引き剥がしたほうが良さそう

まとめ

Ctrl キーを押した時にマウスカーソルが現れ、カメラの回転が固定される
そのカーソルはゲーム内であればカスタマイズされたカーソルとなった
Ctrl キーをもう一度押したらカーソルは消え、カメラの回転が有効になる
カーソルがアクティブになっていることをイベントで知ることができる

まえがきでやりたいと思ったことは、実現できました。

AWS:お金のかからないElastiCache活用術3

simplestar-tech.hatenablog.com
の続き

ローカル環境で Cube データを仮で Redis Mass Insertion するための plotocol を生成する C# コードがこちら

using System.IO;
using System.Text;

namespace CubeWalkDataMaker
{
    class Program
    {
        static void Main(string[] args)
        {
            using (StreamWriter writer = new StreamWriter("data.txt", false, Encoding.Default))
            {
                writer.Write("*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n");
                for (int i = 0; i < 16 * 16 * 16; i++)
                {
                    var key = i.ToString("X6");
                    var val = i.ToString("X8");
                    writer.Write($"*3\r\n$3\r\nSET\r\n$6\r\n{key}\r\n$8\r\n{val}\r\n");
                }
            }
        }
    }
}

できた data.txt を redis の volume マウントしたフォルダに配置して redis に流し込みます。

# cat /redis/data.txt | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 16777217

全部入りましたね
バックアップファイルも作成しましょう

# redis-cli save
OK
root@1faa380aff8b:/data# mv dump.rdb /redis/dump_20190922b.rdb
root@1faa380aff8b:/data# redis-cli dbsize
(integer) 16777216

作られた rdb ファイルのサイズは 270 MB でした。。。

Redis のメモリ使用量はどの程度でしょうか?

root@1faa380aff8b:/data# redis-cli info
# Memory
used_memory:1208828840
used_memory_human:1.13G
used_memory_rss:1239535616
used_memory_rss_human:1.15G
used_memory_peak:1208828840
used_memory_peak_human:1.13G
used_memory_peak_perc:100.00%
used_memory_overhead:806148326
used_memory_startup:792264
used_memory_dataset:402680514
used_memory_dataset_perc:33.33%
allocator_allocated:1208823624
allocator_active:1208979456
allocator_resident:1238646784
total_system_memory:2096144384
total_system_memory_human:1.95G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.00
allocator_frag_bytes:155832
allocator_rss_ratio:1.02
allocator_rss_bytes:29667328
rss_overhead_ratio:1.00
rss_overhead_bytes:888832
mem_fragmentation_ratio:1.03
mem_fragmentation_bytes:30769656
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:49694
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0

# Keyspace
db0:keys=16777216,expires=0,avg_ttl=0

つまり used_memory_peak_human:1.13G ということでした。

最後に AWS の料金の話をして締めます。

オンデマンドノード
>オンデマンドノードでは、長期契約なしに、ノードを実行する時間単位で、メモリ容量に対してお支払いいただきます。

ElastiCache の AWS 料金表によれば 1.13 GB のメモリを持つ最小モデルは
cache.t2.small 0.052USD/h

月々 37.44 USD = 約 4000円の利用料となります。

最後に ElastiCache でバックアップからデータを起こす処理を行ってみましょう。

ElastiCache をローカルバックアップから復元

ローカルに書き出した 270MB の .rdb ファイルを S3 の適当なバケットにアップロードして配置します。

docs.aws.amazon.com

[+ Add account] を選択してその他すべての AWS リージョンの正規IDに読み取り権限を与えて
myBucket/myFolder/myBackupFilename.rdb のようにパスを与えて rdb ファイルを ElastiCache 作成時に指定します。
クラスター内のノードのタイプcache.t2.small を指定しました。

しばらく待ちます。(5分くらい?)
無事ノードが作成され ElastiCache のプライマリエンドポイントが作成されました。

これをコピーして、以前の API Gateway ⇄ ElastiCache の連携の作業につなげます。
simplestar-tech.hatenablog.com
※セキュリティグループの設定が正しいか確認します。

Lambda の実装は次の通り、バックアップから復元したデータが取れるか見るものにしてみました。

from __future__ import print_function
import redis
import uuid

#elasticache settings
r = redis.StrictRedis(host='xxxxxxxxxxxxxxxxxxxxxxxxx.apne1.cache.amazonaws.com', port="6379", db=0)

def handler(event, context):
    data_obtained = r.get('FFFFFF')
    if data_obtained is not None:
        # this print should go to the CloudWatch Logs and Lambda console.
        print ("Success: Fetched value %s from memcache" %(data_obtained))
    else:
        raise Exception("Cannot obtained FFFFFF key data.")
    return { "statusCode": 200, "body": "\"\"" }

Lambda のテスト実行結果、つながって ElastiCache からデータを取得できました。

f:id:simplestar_tech:20190922202042p:plain
Lambda テスト実行結果

curl コマンドからも、インターネット経由で ElastiCache にアクセスできている様子もわかったので、これにて疎通確認完了

まとめ

一番スペックを抑えつつ(お金のかからないようにしつつ)
CubeWalk ゲームに使う block のデータを ElastiCache に全部のせて動作することまで確認できました。

ローカルで作ったバックアップ rdb を S3 に配置して、ElastiCache のノード作成時に渡せることも確認できました。
これでガッツリいじったデータをアップして遊ぶことができますね。

また、定期バックアップを有効にしたので、これを S3 からダウンロードしてローカルで rdb-tools で解析して利用することができます。

以上、お金のかからない ElastiCache 活用術でした。