時代はマルチスレッドプログラミング🤚🤚🤚(だいぶ前から)

この記事はICT委員会 コロナに負けないぞブログリレーの6/3の記事です。


昨日はほなふく君の記事でした。

PC、組んでみませんか?

こういうパーツのレビューって結構助かりますよね。

PCの自作は、個人的感覚だとガンプラより簡単なので

是非組んでみてください。


次回はやがみあん先輩の記事です。

リンク

おいしそう。

僕も自粛期間中結構手のこんだ料理を作ったりしました。

自己紹介

初めまして、芋(@NoNameReUnder)です。

f:id:potato0022:20200602000154p:plain
これはVKet4で友人が撮ってくれた自画像です

普段はキャラモデリングやイラストの仕事をしたりしつつ、Unityを用いてゲームを作ったりしています。

VALORANT楽しい

使用ツール

Unity ZBrush Blender ClipStudioPaint

普段使いしている言語

Unity C#


新一年生(いるのかは把握していませんが)は、なんか変な人いるなくらいで覚えてもらえると嬉しいです。

急なダイレクトマーケティング「アライブサバイブ」

4人プレイのボードゲーム

¥1500 JPY 予定 f:id:potato0022:20200602074111p:plain

広報Twitter

内容物

同盟・戦争カード   5枚

資源カード      25枚

キャラクターカード   5枚

説明カード      1枚

ゲームの概要

このゲームは謎のウイルスにより荒廃した世界の中で、自身に割り振られたキャラクターの特性を使い資源を集めていきます。

しかし、この世界にはプレイヤー全員が生き残れるような資源は残っていません。

そのため、ほかのプレイヤーと組んだり、交渉したり、時には戦って生き残りましょう。


8月に販売開始する予定なので、発売したら是非買って遊んでみてください。

僕の春休みはこのボドゲのタスクで消えました。悲しいね、、、

JobSystemの解説

本題に戻ります。

今回はUnityを触ったことがある人に対する記事ですので、興味がない人はブラウザバックしてください。

読んでも面白くないからね。


事前に、以下の動画・スライドを見たことがある方は、ほんとに読む必要無いです。


Unite Europe 2017 - C# job system & compiler


Job Systemとは

f:id:potato0022:20200602072113j:plain

Unity2017で公開されたマルチスレッドプログラミング手法

JobSystemのここが良い

(割と)簡単に書ける

GCフリー

レースコンディション・デッドロックが起こりうる場面はあらかじめエラーをはいてくれる(安全)

IL2CPPコンパイラを用いたマルチスレッドプログラムの最適化によるパフォーマンス向上

2017のデモでは、200,000個のGameObjectを動かした際のパフォーマンス比較ではシングルスレッド実行時の116倍の速さをみせています

JobSystemの注意点

データ構造はstructのみ

すべてが早くなるわけではない

Jobの発行にもコストは必要なので、発行しすぎるとかえって重くなる

Job Systemは、Unityがデータ指向の考えに代わってきており、

Unity2019から公開されたECSとの連携を目的として設計されているため、構造が少し特殊です。

サンプルコード

任意のGameObjectをマルチスレッドで動かすためのサンプルコードを下記に記述しています。

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Jobs;
using Unity.Jobs;

public sealed class BounceCube5 : MonoBehaviour 
{
    /// <summary>
    /// 動かすオブジェクトのTransform格納用配列
    /// </summary>
    [SerializeField] Transform[] targets;

    /// <summary>
    /// NativeArray<T>は
    /// JobSystemJobSystemでアクセスするための記述方
    /// デッドロックを防ぐため、値を渡すときにメモリアドレスを参照させるのではなく、
    ///値を複製し元の値とは別の独立した動きをとります。
    /// Gitのブランチのようなイメージです。
    /// この配列はECSとの連携を目的としているため、連続したメモリアドレスになっています。
    /// float型の配列velocityを用意
    /// </summary>
    NativeArray<float> velocity;
    /// <summary>
    /// RaycastCommand型配列command
    /// </summary>
    NativeArray<RaycastCommand> commands;
    /// <summary>
    /// RaycastHit型配列results
    /// </summary>
    NativeArray<RaycastHit> results;
    /// <summary>
    /// int型hitQueue
    /// </summary>
    NativeQueue<int> hitQueue;
    /// <summary>
    /// Job SystemでTransformにアクセスするための配列
    /// TransformAccessArray型愛列transformArray
    /// </summary>
    TransformAccessArray transformArray ;
    /// <summary>
    /// Struct IsHitGroundJob Jobの宣言
    /// </summary>
    IsHitGroundJob hitCheckJob;
    /// <summary>
    /// Job発行のためのJobHandleを宣言
    /// </summary>
    JobHandle handle;

    /// <summary>
    /// スクリプトをアタッチしているオブジェクトがActive状態になっていれば実行
    /// </summary>
    void OnEnable()
    {
        velocity = new NativeArray<float>(targets.Length, Allocator.Persistent);
        commands = new NativeArray<RaycastCommand>(targets.Length, Allocator.Persistent);
        results = new NativeArray<RaycastHit>(targets.Length, Allocator.Persistent);
        hitQueue = new NativeQueue<int>(Allocator.Persistent);

        transform.DetachChildren();

        for (int i=0; i< targets.Length; i++)
        {
            velocity[i] = -1;
        }
        transformArray = new TransformAccessArray(targets);

        hitCheckJob = new IsHitGroundJob()
        {
            raycastResults = results,
            result = hitQueue.ToConcurrent()
        };

    }
    /// <summary>
    /// スクリプトをアタッチしているオブジェクトが非Active状態になっていれば実行
    /// </summary>
    void OnDisable()
    {
        ///Jobの開放
        handle.Complete();

        ///メモリの開放
        ///メモリリークを防ぐため、必ず実行
        velocity.Dispose();
        commands.Dispose();
        results.Dispose();
        hitQueue.Dispose();
        transformArray.Dispose();
    }
    /// <summary>
    /// update関数実行後に実行
    /// </summary>
    void LateUpdate()
    {
        ///Jobの開放
        handle.Complete();

        // Raycastの開始点と位置を設定
        for (int i=0; i<transformArray.length; i++)
        {
            var targetPosition = transformArray[i].position;
            var direction = Vector3.down;
            var command = new RaycastCommand(targetPosition, direction);
            commands[i] = command;
        }

        // 移動のコマンドを設定
        var updatePositionJob = new UpdateVelocity()
        {
            velocitys = velocity
        };

        //GameObjectのTransformを設定
        //回転・位置を変更
        var applyTrans= new ApplyTrans()
        {
            velocitys = velocity
        };

        // 並列処理を実行(即完了待ち)
        // 終わったらコマンドに使ったバッファは不要なので破棄
        handle = RaycastCommand.ScheduleBatch(commands, results, 20);
        handle = hitCheckJob.Schedule(transformArray.length, 20, handle);
        handle = new ReflectionJob { velocitys = velocity, result=hitQueue }.Schedule(handle);
        handle = updatePositionJob.Schedule(transformArray.length, 20, handle);
        handle = applyTrans.Schedule(transformArray, handle);

        //Jobの開放
        handle.Complete();
    }

    struct UpdateVelocity : IJobParallelFor
    {
        public NativeArray<float> velocitys;

        void IJobParallelFor.Execute(int index)
        {
            velocitys[index] -= 0.098f ;
        }
    }

    struct ApplyTrans : IJobParallelForTransform
    {
        [ReadOnly] public NativeArray<float> velocitys;

        void IJobParallelForTransform.Execute(int index, TransformAccess transform)
        {
            transform.localRotation = Quaternion.Euler(transform.rotation.x + velocitys[index] * 180f, transform.rotation.y + velocitys[index] * 180f, transform.rotation.z + velocitys[index] * 180f);
            transform.localPosition += Vector3.up * velocitys[index];
        }
    }
    

    struct IsHitGroundJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<RaycastHit> raycastResults;
        [WriteOnly] public NativeQueue<int>.Concurrent result;

        void IJobParallelFor.Execute(int index)
        {
            if (raycastResults[index].distance < 1f)
            {
                result.Enqueue(index);
            }
        }
    }

    struct ReflectionJob : IJob
    {
        public NativeQueue<int> result;
        public NativeArray<float> velocitys;

        public void Execute()
        {
            while(result.TryDequeue(out int index))
            {
                velocitys[index] = 2;
            }
        }
    }
}

今回は、このスクリプトを用いて802個のオブジェクトを動かしてみました。 以下が実行結果です。 f:id:potato0022:20200602202517p:plain f:id:potato0022:20200602202513p:plain


動作結果

まとめ

Job SysytemはUnityプログラマーを目指す場合、確実に必要な知識です。

まずはオブジェクトごとに処理をするのではなく、まとめて計算できる箇所はどんどんまとめて処理をする書き方を身に着けていきましょう。

これは違うだろとうの意見があればコメントをくださると有り難いです。

ここまで読んでくださりありがとうございました。