20210322のUnityに関する記事は5件です。

[Unity]マウスポインタでECSエンティティをさわる(実行時)

動作確認環境

Unity 2020.02
Entities 0.17.0

前提

さわる対象のEntityはPhysics Shape相当(Colliderでもさわれる)がないとだめです。
#Physics Bodyは不要の簡易版

ソースコード

2パターン。(挙動的には同一のもの)
・Monobehavior経由
・ECS、Job、Burstで実行(無駄に長い)
 カメラ情報を取得するためにMonobehaviorを使います。

MouseTouchEntityFromMonobehavior.cs
    [SerializeField] Camera xCamera = default;

    const float RAYCAST_DISTANCE = 1000;
    PhysicsWorld physicsWorld => World.DefaultGameObjectInjectionWorld.GetExistingSystem<BuildPhysicsWorld>().PhysicsWorld;
    EntityManager entityManager => World.DefaultGameObjectInjectionWorld.EntityManager;
    void LateUpdate()
    {
        var screenPointToRay = xCamera.ScreenPointToRay(position);
        var rayInput = new RaycastInput
        {
            Start = screenPointToRay.origin,
            End = screenPointToRay.GetPoint(RAYCAST_DISTANCE),
            Filter = CollisionFilter.Default
        };
        if (!physicsWorld.CastRay(rayInput, out RaycastHit hit)) return;
        var selectedEntity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
        // ↑ マウスがあたったエンティティ(単体)
        // CastRayの2個目の引数でNativeList<RaycastHit>を入れてあげれば
        // Start-End間のEntitiesを取れる。(検証してないけど)

        // あたったコンポーネントにアクセッス!
        var renderMesh = entityManager.GetSharedComponentData<RenderMesh>(selectedEntity);
    }
MouseTouchEntityFromJob.cs
    public class MouseTouchEntityFromJob : MonoBehaviour, IConvertGameObjectToEntity
    {
        [SerializeField] Camera xCamera = default;
        MouseTouchEntityFromJobSystem system => World.DefaultGameObjectInjectionWorld.GetExistingSystem<MouseTouchEntityFromJobSystem>();
        void IConvertGameObjectToEntity.Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
        {
            dstManager.AddComponentData(entity, new MouseTouch() {});
        }
        void Start()
        {
            system.xCamera = xCamera;
        }
    }
    public struct MouseTouch : IComponentData {}
    public struct HitTargetData
    {
        public Entity Entity;
        public int Hit;
    }

    public class MouseTouchEntityFromJobSystem : SystemBase
    {
        public Camera xCamera = default;
        const float RAYCAST_DISTANCE = 1000;
        public NativeArray<HitTargetData> MouseHitData;
        BuildPhysicsWorld m_BuildPhysicsWorldSystem;
        EndFramePhysicsSystem endFramePhysicsSystem;
        EntityQuery m_MouseGroup;

        public MousePickSystem() 
        {
            // 気持ち悪かったらOnStartRunning()内でもいい好きにして!
            MouseHitData = new NativeArray<HitTargetData>(1, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            MouseHitData[0] = new HitTargetData();
        }
        protected override void OnStartRunning()
        {
            // OnCreate()でもいい。好きにして!
            m_BuildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
            endFramePhysicsSystem     = World.GetOrCreateSystem<EndFramePhysicsSystem>();
            m_MouseGroup = GetEntityQuery(new EntityQueryDesc { All = new ComponentType[] { typeof(MouseTouch) } });
        }
        protected override void OnUpdate()
        {
            var handle = JobHandle.CombineDependencies(Dependency, endFramePhysicsSystem.GetOutputDependency());
            if (Input.GetMouseButton(0))
            {
                MouseTouchExecute(ref handle);
                PaintJobHandle = handle;
                handle.Complete();
            }
            Dependency.Complete();
            if(MouseHitData[0].Hit == 1)
            {
                // あたったコンポーネントにアクセッス!
                var renderMesh = EntityManager.GetSharedComponentData<RenderMesh>(MouseHitData[0].Entity);
            }
        }
        protected override void OnDestroy()
        {
            if (MouseHitData.IsCreated) MouseHitData.Dispose();
        }

        void MouseTouchExecute(ref JobHandle handle)
        {
            var screenPointToRay = xCamera.ScreenPointToRay(Input.mousePosition);
            handle = new Touch
            {
                physicsWorld = m_BuildPhysicsWorldSystem.PhysicsWorld,
                SpringData = MouseHitData,
                RayInput = new RaycastInput
                {
                    Start = screenPointToRay.origin,
                    End = screenPointToRay.GetPoint(RAYCAST_DISTANCE),
                    Filter = CollisionFilter.Default
                },
            }.Schedule(handle);
        }
        [BurstCompile]
        struct Touch : IJob
        {
            [ReadOnly] public PhysicsWorld physicsWorld;
            public NativeArray<HitTargetData> SpringData;
            public RaycastInput RayInput;

            public void Execute()
            {
                if (physicsWorld.CastRay(RayInput, out Unity.Physics.RaycastHit rayInput))
                {
                    RigidBody hitBody = physicsWorld.Bodies[rayInput.RigidBodyIndex];
                    SpringData[0] = new HitTargetData
                    {
                        Entity = hitBody.Entity,
                        Hit = 1,
                    };
                }
                else
                {
                    SpringData[0] = new HitTargetData 
                    {
                        Hit = 0,
                    };
                }
            }
        }
    }

参考資料

https://github.com/reeseschultz/ReeseUnityDemos
https://github.com/Unity-Technologies/EntityComponentSystemSamples

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】ある水平軸に対して指定オブジェクトが上側にあるか、下側にあるかの判定方法

はじめに

タイトルの通り、ある水平軸に対して指定オブジェクト(今回はカメラ)が上側にあるか、下側にあるかを判定する方法についてまとめます。
上下判定01.png

Script及び注意点

まず、スクリプトから

    public bool  IsUp(Transform Target,Transform Cam)
    {
        var diff = Target.transform.position - Cam.transform.position;
        var axis = Vector3.Cross(Target.transform.forward,diff);
        return axis.x > 0;
    } 

判定自体はこの三行で行えます。

しかし、注意点として例えばTargetの原点が以下のように足元(0地点)にある場合、Target.transform.fowrdで求まる水平軸は足元にくるため、カメラが真下に潜り込まない限り基本的には判定は常にTrueが返ってきてしまいます。
上下判定02.png
よって、Targetの中央高さを基準水平軸としたい場合は、Targetの子として空のオブジェクトを配置し、そのオブジェクトをTargetにセットすることで解決します。
上下判定03.png

なぜ、この判定を行う必要があったのか

そもそもなぜこのような判定方法を取る必要があったかというと、Unityにおいて回転の値はEditor上で入力する「-50」と「310」は全く同じ角度となります。

しかしながら、この値をスクリプトから取得すると、返ってくるのはどちらの値でも「310」が返ってきます。
こうなると何が困るかというと、例えばCam01からCam02に回転値「-50」を引き継ぐ場合、以下のような事が起こりえます。
上下判定04.png
そのため、何らかの方法で回転値を判定し、正しい値を受け渡す処理が必要だったわけです。

今回はベクトルから上下を判定しましたが、調べた中でもう一つ簡単に判定する方法も見つけたので、併せて紹介しておきます。

おまけ(回転値を+180~-180に整える)

    public float AdjustAngle180(float angle)
    {
        float subNormal = Mathf.Floor((angle + 180f) / 360f) * 360f;
        return (angle > 0f) ? angle - subNormal : angle;
    }

angleに回転値を渡します、渡す際はtransform.rotation.eulerAnglesでQuaternion型からVecotr3型にキャストしてから渡しましょう。
subNormalは「0」または「360」となります、水平軸より上側なら「0」、下側なら「360」です。

これを0以上かどうか判定し、0以上の場合は取得したangleから-360してマイナスの値に、そうでなければそのままの値を返します。
上下判定05.png

結果としては先述したベクトルから求める方法と同じ結果となるため、どちらを使っても問題ないと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでFacebookSDKのAndroidでログインエラーの解決方法

概要

UnityでFacebookSDKを使用してAndroidでログインエラーが出てしまう問題の解決方法

問題点

KeyStoreの作り方がFacebookで紹介されているものと別であったため、Unity上で表示されたキーハッシュが間違っていた

解決方法

確実なキーハッシュの取得方法はIResultのRawResultの中に記載されている

// 初期化等は割愛

FB.LogInWithReadPermissions(・・・, OnLogin);


void OnLogin(IResult result)
{
    // こいつを確認する!!!
    Debug.Log($"Raw:[{result.RawResult}]");
}

その他エラー解消のための参考文献

公式
ザ・ゆるふわ
cloudpack.media

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity で PlayFab Party を使う時の注意点

はじめに

PlayFab Party を導入する際に注意しないといけない内容をまとめておきたいと思います。

※ちなみに Mac 用の dll が用意されていないため Mac 環境の Unity エディターでは使用することが出来ません。

Unity のセットアップ

Unity プラグインのインポート

上記から最新版の unitypackage をダウンロードしてインポートします。

※ここで注意しないといけないのは PlayFab Party のパッケージ内に PlayFab SDK も含まれています。もし、PlayFab Party をインポートする前に最新の PlayFab SDK をインポートしている場合は PlayFab Party の方に含まれる PlayFab SDK の方が古い場合がありますので、上書きする際には注意してください。

Unity 側の設定

Unity で使用するには『Allow 'unsafe' Code』を有効にする必要があります。

スクリーンショット 2021-03-21 13.17.14.png

Android 用の設定

Android のビルドの際に以下のようなエラーが表示される事があります。

image.png

これは PlayFab Party をインポートすると Custrom Main Gradle Templete が有効になりますが、使用している Unity のバージョンによっては mainTemplete.gradle の設定が合っていないため発生します。
これを解消するには Assets/Plugins/Android/mainTemplete.gradle を以下のように書き換えます。

aaptOptions {
    // この行を追加
    noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
    ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
}**PACKAGING_OPTIONS**

また以下のエラーが発生する場合は gradle.properties の更新が必要になります。

FAILURE: Build failed with an exception.

* What went wrong:
Could not determine the dependencies of task ':launcher:lintVitalRelease'.
> This project uses AndroidX dependencies, but the 'android.useAndroidX' property is not enabled. Set this property to true in the gradle.properties file and retry.

Gradle ファイルを出力の際に AndroidX と Jetifier を有効にする設定を gradle.properties に追加します。
このエラーが発生するのは PlayFab Party 以外のプラグインで gradle.properties を出力しているのが原因です。

public int callbackOrder
{
    // 最後に実行されるようにする
    get => 999;
}

// IPostGenerateGradleAndroidProject を継承したクラスに実装する
public void OnPostGenerateGradleAndroidProject(string path)
{
    // path には gradle の出力先が渡されてくるが gradle.properties はその上の階層に配置されている
    using (var stream = File.AppendText(Path.Combine(path, "..", "gradle.properties"))) {
        stream.WriteLine("");
        stream.WriteLine("android.useAndroidX=true");
        stream.WriteLine("android.enableJetifier=true");
    }
}

おわりに

導入の際にはこれぐらいの問題点ですぐにマルチプレイが実装できますので一度 PlayFab Party の導入を検討してみてはいかがでしょうか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BlenderとPythonとUnityを用いて、巨大な立体迷路を作成する。

概要

この度、このようなゲームを作りました。基本的には迷路のゲームです。(サイトのリンク)
messageImage_1616335316395.jpg
messageImage_1616335318339.jpg
本記事ではこのゲームの製作過程を掲載すると共に、様々な分野の方に対しきっと有益になるだろうという情報をまとめてあります。楽しんで頂けたら幸いです。

Step0 前提

まず用語の整理を。

  • Blender : 3DCG制作ソフト。Pythonによって操作が可能になっています。

  • Python : 言わずと知れた有名プログラミング言語。

  • Unity : ゲーム制作ソフト。スタート画面の表示やゲームオーバーの判定などをしてくれます。言語はC#です。

大まかな流れとしては、
Step1. Blenderで3Dオブジェクトを作成
Step2. Pythonでそれを迷路に組み立てる
Step3. Unityでゲームとして完成させる
という風になっています。

コードに関しては、読みやすさも考え記事中においては一部抜粋に留めています。もし全体のコードを知りたい場合はプルダウン内をご覧ください。そこに掲載してあります。

さて、記事本編に入る前に少しだけ前置きを。そもそも私がこの迷路を作ろうと思ったのは、昨年の夏頃にエッシャーの相対性という作品を3Dで作成したことがきっかけです。このような世界観に似たものを独自に作ろうと思ったのが前提としてあります。
イラスト.jpg
本ゲームが、重力があいまいで、階段や橋などで各所が連結され、色がモノクロに統一されているなどといった特徴を有しているのは、そういった事情からとなっています。

Step1 3Dオブジェクトの作成

まず最初に大枠として、球を内に含む立方体のようなものを作成します。
このコードを実行すると、
messageImage_1615002419349.jpg
このように、
messageImage_1615002384682.jpg
球に従った形が出来ます。(上図は立面図です)
この中をプレイヤーに動いてもらうことで迷路にしようという魂胆です。

ただ、これだけでは機械的すぎるので、randomから正規分布に従ったランダムさを生成してくれるrandom.normalvariateを使用して、有機的にします。

#各直方体(島)の高さに平均0、標準偏差(σ)1のランダムさを与える
height=[height[Y][X]+round(random.normalvariate(0,1)) for X in range(num_of_blocks)] for Y in range(num_of_blocks)]

この処理をすると、
messageImage_1615021893993.jpg
ランダムさが出てきてそれらしさが増します。こういう処理がサラッと出来る辺り、pythonの恩恵をとても感じます。Blenderのその他の機能も便利ですが、ランダムさに関してはscriptingの右に出る者はいません。

そしてこの各島の間を移動してもらうために橋や階段を掛けていきます。
具体的には、隣り合う島同士の高さの差が、

  • 0m違いならば、橋を

  • 1m違いならば、階段を

  • 2m違いならば、スロープを

と言う風にしています。
messageImage_1616316465448.jpg

作業途中はこんな感じで、プロトタイプ感あふれています。
messageImage_1615109316907.jpg

前座としてはこの辺り、次からがこの記事のメインとなります。

Step2 pythonで経路探索

まず初めに、Step1で完成したものの一部を、上から見て頂きましょう。
messageImage_1616329932271.jpg

……。よーく見ると……。全然繋がっていないですね。 基本的に中央の島から出発して四隅のいずれかに行ってもらう予定だったのですが、これではどこにも行けません。そもそも中央の直方体が絶海の孤島です。ここをスタートにしては一歩すら無理です。
それもそのはず、これではまだ高さの情報をもとに適当に橋などを繋いだにすぎません。これではダメダメです。

そもそも、迷路作成と言えば、棒倒し法穴掘り法壁伸ばし法などが有名なのですが(これらの方法が分かりやすく解説されたサイトのリンクを記事最後に貼っています。興味があれば是非。)、これらの方法は前提としてスタートからゴールまで行ける事を保証しています。

ただ、今回の場合は既に経路を作成してしまっていて、実際にゴールできるかの保証はどこにもありません。では、このように経路を先に決めてしまった上で、ゴールまで到達可能迷路を作ることは出来ないのでしょうか?
結論から言えば、その反例が今回の事例です。

方針としては、ランダムかつ大量に経路を作成した上で、ゴールできるかを経路探索し、可能ならばそれを採用するという「数撃ちゃ当たる」の精神で行きます。

ダイクストラ法とは

今回はダイクストラ法と言う方法を用いてその判定を行いました。(最終的にはその情報を利用しなかったのですが)スタートからゴールまでの最短経路も合わせて算出しようと思い、この方法を採用しました。

ダイクストラ法というのは最短経路問題を解くためのアルゴリズムで、競技プログラミングの文脈で比較的よく出てくるようです。というか、私自身競プロを通じてダイクストラ法を知りました。

wikiの概要には次のような説明がなされています。

ダイクストラ法はグラフ上の2頂点間の最短経路を求めるアルゴリズムで、1959年エドガー・ダイクストラによって考案された。 応用範囲は広くOSPFなどのインターネットルーティングプロトコルや、カーナビの経路探索や鉄道の経路案内においても利用されている。

具体的なアルゴリズムの内容を知りたい方はこのサイトとか、このサイトとかも合わせてご覧ください。

私は残念ながら競プロ知識に全くあかるくないので、極めて不正確な説明で申し訳ないですが、簡単に言うと、「スタートからの最短距離が分かっている場所から最短で行けるなら、そこもスタートから最短だろ」みたいなことです。(間違っていたら競プロ強い方から指摘が飛んでくると信じている)

このアルゴリズムは、一般的には優先度付きキュー(heapq)などを用いて実装するのですが、pythonの場合scipyというライブラリを使うと超簡単にダイクストラ法を実行してくれます。私みたいな馬鹿にも優しい!ありがたい!!

scipyのダイクストラ法を走らせてみる

それでは実際に走らせてみましょう。(更なる詳細はこのサイトこのサイトをご覧ください。) 二次元行列をそのまま渡すことは出来ないので、それぞれの島に対してindex_for_dijkstraという関数で番号を割り振った上で、実行していきます。

row_np=np.array(row)
col_np=np.array(col)
data_np=np.array(data)
graph=csr_matrix((data_np, (row_np, col_np)), shape=(num_of_blocks**2, num_of_blocks**2)).toarray()
distance=dijkstra(graph, directed=False, indices=[index_for_dijkstra(x,y) for x,y in [(0,0),(0,num_of_blocks-1),(num_of_blocks-1,0),(num_of_blocks-1,num_of_blocks-1)]])

実行結果(の一部)がこちらです。
image.png
infとは無限大、つまり到達不可能であることを示しています。下のケース(左下の島からの最短経路距離を示したもの)はinfだらけなので、孤立していることが分かります。
一方、上のケース(左上の島からの最短経路距離を示したもの)は、数字が多く書き込まれています。これは橋や階段に対して事前に与えた移動距離の総和を示していると共に、「数字が書き込まれいる⇔到達可能である」ということも示しています。

これを使えば、生成された迷路がゲームに適しているか分かりますね! 今回は四隅から中央地点までの距離が全てinfでない、つまり到達可能であるならば合格としました。

最終的な実行

この条件をもとに試行を繰り返していきます。
コマンドプロンプト上を文字列がダーッって流れていく様を眺めるのはなんか楽しいですね。ループが1000回を超えて試行されたケースはありませんでした。体感ですが長くて5秒で一個の迷路が完成していきます。
messageImage_1616204002984.jpg

出来上がったものがこちらです。
messageImage_1616329933963.jpg

……。……よーく見てください。……全部つながっていますね!それもいい感じに!万歳!
これで程よくプレイヤーが迷ってくれそうな迷路が完成しました。さらにコードは完成しているので、大量生産もお手軽です。

(ただ、公平な情報発信の為に記しておくと、この方法はあまりお勧めできるものではありません。 なぜか? それは最悪計算量が$\mathcal{O}(∞)$だからです。この方法が5秒(実質的にはBlenderの処理がメインなので1秒以下)で迷路を作れているのは、橋や階段がある程度の数かかっているという前提が必要です。もしもその数が少なければ計算は10分経っても終わることはないでしょう。製作方法としては先述の穴掘り法などの方が遥かに優秀です
ただ、今回の様に経路を先に決めている場合にはそこそこ有効だと思いますし、ボゴソートよりは遥かに終わる見込みのある$\mathcal{O}(∞)$です。)

以上がStep2でした。

Step1,2のおまけ

二つに分けています。どちらもそこそこ長いです。

最終的に使用したコード全文

一応注意書きですが、このコードをそのまま実行してもエラーしか出ません。理由は三つあり、一つ目がパス名(偽名入れてますし、私の場合のパス名です)、二つ目がimport scipy(理由はおまけに)、そして三つ目がコレクションの存在を前提としていることです。具体的に言うとScene Collection直下のDONT DELETEという名前のコレクション、そしてそれに属するBRIDGE,DIAGONAL STAIRS,SLOPE,STAIRS,TREASURE CHESTという五つのオブジェクトの存在が前提です。またデバッグに用いたprint文なども、消すべきかもしれませんが作業の上ではかなり本質な要素だったのであえてそのままにしています。そしてbpyはblenderをscriptから操作する上でのAPIとなっています。これらの点を頭に入れながらお読み下さい。
import bpy
import math
import random
import datetime
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.csgraph import shortest_path,dijkstra
print("----------------------------------------------------------------------")

#数値決め
#今回は試行錯誤が出来る様、やや過剰に数値に名前を与えています。 pep-8破っているところも多いですが、多めに見て頂ければ……
num_of_blocks=9 #奇数
num_of_height_variation=6+0 #0を元々の高さとしてこの数分だけマイナスの差分がある
cuboid_sidelength=3 #直方体の短い方の一辺の長さ
foundation_height=60 #>=max(2*negative_length)
HEIGHT_OF_EACH_STEP=4 #直方体同士のデフォルトの高さの差 固定値
scale_factor=region=9 #直方体同士の間隔に等しい ≒math.sqrt(2)*HEIGHT_OF_EACH_STEP*num_of_height_variation/(num_of_blocks*(math.sqrt(2)-1))
#print(math.sqrt(2)*HEIGHT_OF_EACH_STEP*num_of_height_variation/(num_of_blocks*(math.sqrt(2)-1)))
entire_cube_sidelength=region*num_of_blocks #球に内接する立方体の一辺の長さ
entire_sphere_radius=entire_cube_sidelength*math.sqrt(3/4) #直方体たちが形成する球の半径
last_adjustment=2 #計算をミスってしまっていたので、最後の調整用の数値です。
isfinal=True

########################################################################

def func_z(X,Y): #XYはマス目の番号 (X:-(num_of_blocks-1)//2~+(num_of_blocks-1)//2 Y:-(num_of_blocks-1)//2~+(num_of_blocks-1)//2)
    x=abs(X)+0.5 #xyは使用する座標の絶対値
    y=abs(Y)+0.5
    initial_height=math.sqrt(num_of_blocks**2-x**2-y**2)-num_of_blocks/math.sqrt(2)
    negative_height=math.floor((num_of_height_variation/(num_of_blocks-num_of_blocks/math.sqrt(2)-0.1))*initial_height)*last_adjustment
    individual_height=foundation_height-HEIGHT_OF_EACH_STEP*negative_height//last_adjustment #土台を用意し、negative分だけ削る
    return individual_height
def isinside(x,y): #list index out of range避け
    return True if (0<=x<num_of_blocks) and (0<=y<num_of_blocks) else False
def index_for_dijkstra(x,y):
    index=x+y*num_of_blocks
    return index
def reverse_index_for_dijkstra(index,is_for_direction=False):
    x=index%num_of_blocks
    y=index//num_of_blocks
    if is_for_direction:
        x=(x if x!=num_of_blocks-1 else -1) 
        y=(index-x)//num_of_blocks
    return (x,y)

is_good_maze=False

for highest_counter in range(1000 if isfinal else 1):
    #球に従うように各直方体の高さを仮置きする
    height=[[func_z(X,Y) for X in range(-(num_of_blocks-1)//2,(num_of_blocks-1)//2+1)] for Y in range(-(num_of_blocks-1)//2,(num_of_blocks-1)//2+1)]

    #各直方体の高さに平均0、標準偏差(σ)1のランダムさ(*last_adjustment)を与える
    special_cells=[(0,0),(0,num_of_blocks-1),(num_of_blocks-1,0),(num_of_blocks-1,num_of_blocks-1),((num_of_blocks-1)//2,(num_of_blocks-1)//2)]
    height=[[height[Y][X] if (X,Y) in special_cells else height[Y][X]+round(random.normalvariate(0,1))*last_adjustment for X in range(num_of_blocks)] for Y in range(num_of_blocks)]

    #角に隣接する直方体について、高さが1*last_adjustmentの差分しかないようにする(つまり、ゴールしやすくする)
    for X,Y in special_cells[:4]:
        if height[Y][1 if X==0 else (num_of_blocks-1)-1]!=foundation_height-1*last_adjustment and height[1 if Y==0 else (num_of_blocks-1)-1][X]!=foundation_height-1*last_adjustment:
            height[Y][1 if X==0 else (num_of_blocks-1)-1]=foundation_height-1*last_adjustment

    row=[] #dijkstra用の辺の情報を表すリスト
    col=[]
    data=[]

    #橋や階段などのオブジェクトを3次元空間上にどう配置するかのデータを定める
    #ここから先4回同じ構造のコードが続きます。関数とか定義すれば良かったのですが、
    #それぞれの相違を反映させるのが少し面倒なのでこうしました。
    deleting_rate=0.3

    dif0=[]#BRIDGE
    dif0_deleted=0
    for X in range(num_of_blocks):
        for Y in range(num_of_blocks):
            for x,y,angle in [(1,0,0),(0,1,-math.pi/2)]: #rotationより、DONT DELETE内のオブジェクトは全てx軸正方向を向くことが要請される
                if isinside(X+x,Y+y) and height[Y+y][X+x]==height[Y][X]:
                    if (num_of_blocks//3<=X<2*num_of_blocks//3) and (num_of_blocks//3<=Y<2*num_of_blocks//3) and random.random()<deleting_rate:
                        dif0_deleted+=1
                        continue
                    dif0.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,height[Y][X],angle))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(600) #辺の重み(つまり移動コスト) 長さをcm単位で計測しました
    dif0=[(x*scale_factor,y*scale_factor,z,-angle) for x,y,z,angle in dif0] #angleにマイナスが付いている理由はおまけで。角度関連はかなり苦肉の策が多いです。

    dif1=[]#STAIRS
    dif1_deleted=0
    for X in range(num_of_blocks):
        for Y in range(num_of_blocks):
            for x,y,angle in [(1,0,0),(0,1,-math.pi/2)]:
                if isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==1*last_adjustment:
                    if (num_of_blocks//3<=X<2*num_of_blocks//3) and (num_of_blocks//3<=Y<2*num_of_blocks//3) and random.random()<deleting_rate:
                        dif1_deleted+=1
                        continue
                    dif1.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(683)
                elif isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==-1*last_adjustment:
                    dif1.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle+math.pi))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(683)
    dif1=[(x*scale_factor,y*scale_factor,z,-angle) for x,y,z,angle in dif1]

    dif1_diag=[]#DIAGONAL STAIRS
    for X in range(num_of_blocks):
        for Y in range(num_of_blocks):
            for x,y,angle in [(1,1,-math.pi/4),(1,-1,math.pi/4)]:
                if ((num_of_blocks-1)//2,(num_of_blocks-1)//2) in ((X,Y),(X+x,Y+y)):
                    continue #中央地点から対角線上に経路が伸びてほしくないので
                if isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==1*last_adjustment:
                    dif1_diag.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(969)
                elif isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==-1*last_adjustment:
                    dif1_diag.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle+math.pi))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(969)
    dif1_diag=[(x*scale_factor,y*scale_factor,z,-angle) for x,y,z,angle in dif1_diag]
    temp_dif1_diag_len=len(dif1_diag)
    #https://note.nkmk.me/python-list-unique-duplicate/ 参考
    dif1_diag_seen=[] #二つの階段が交差してしまっている場合、片方を取り除く
    dif1_diag=[(x,y,z,angle) for x,y,z,angle in dif1_diag if (x,y) not in dif1_diag_seen and not dif1_diag_seen.append((x,y))]

    dif2=[]#SLOPE
    for X in (0,1,num_of_blocks-3,num_of_blocks-2): #元々の範囲がnum_of_blocksまでのため-1,さらにx,yの値が正だからさらに-1,つまり-2
        for Y in (0,1,num_of_blocks-3,num_of_blocks-2): #num_of_blocks=9の時用にX,Yはそれぞれ4個までにしている。場合によっては増減させる
            for x,y,angle in [(1,0,0),(0,1,-math.pi/2)]:
                if isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==2*last_adjustment:
                    dif2.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(721)
                elif isinside(X+x,Y+y) and height[Y+y][X+x]-height[Y][X]==-2*last_adjustment:
                    dif2.append((-(num_of_blocks-1)//2+X+x/2,-(num_of_blocks-1)//2+Y+y/2,(height[Y+y][X+x]+height[Y][X])/2,angle+math.pi))
                    row.append(index_for_dijkstra(X,Y))
                    col.append(index_for_dijkstra(X+x,Y+y))
                    data.append(721)
    dif2=[(x*scale_factor,y*scale_factor,z,-angle) for x,y,z,angle in dif2]


    #####dijkstra#####
    row_np=np.array(row)
    col_np=np.array(col)
    data_np=np.array(data)
    graph=csr_matrix((data_np, (row_np, col_np)), shape=(num_of_blocks**2, num_of_blocks**2)).toarray()
    #https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html
    #https://note.nkmk.me/python-scipy-shortest-path/ 参考
    distance=dijkstra(graph, directed=False, indices=[index_for_dijkstra(x,y) for x,y in [(0,0),(0,num_of_blocks-1),(num_of_blocks-1,0),(num_of_blocks-1,num_of_blocks-1)]])
    distance_mid=[distance[i][index_for_dijkstra((num_of_blocks-1)//2,(num_of_blocks-1)//2)] for i in range(4)]

    #中央地点に到達可能か
    print("highest_counter:",highest_counter,"  number of reachable corners:",4-distance_mid.count(float('inf')))
    if 4-distance_mid.count(float('inf'))==4:
        print("congratulations!!!!!")
        is_good_maze=True
        break
#以上までがhighest_counterによるループ

if not is_good_maze:
    print("There was not any good mazes.\nTRY AGAIN")
    #当たり前ですが、ここでsys.exitを使うとこのスクリプトのみならずblender自体が終了します。
    #私は何も考えずにそれをやらかして!?!?となりました。
    #以下elseで分岐してもいいですが、インデントが嫌なので今回は続行しています。

#print("height:",height,"\n")
#print("dif0:",dif0)
#print("len(dif0)=",len(dif0),"(deleted=",dif0_deleted,")\n")
#print("dif1:",dif1)
#print("len(dif1)=",len(dif1),"(deleted=",dif1_deleted,")\n")
#print("dif1_diag:",dif1_diag)
#print("len(dif1_diag)=",len(dif1_diag),"(deleted=",temp_dif1_diag_len-len(dif1_diag),")\n")
#print("dif2:",dif2)
#print("len(dif2)=",len(dif2),"\n")
#print("row:",row)
#print("col:",col)
#print("data:",data)

rowcol=[[row[i],col[i]] for i in range(len(row))]
#print("(row,col)-->\n",rowcol,"\n")
rowcol_flatten=sum(rowcol,[])

def isconnected(i):
    for j in range(4):
        if distance[j][i]!=float('inf'):
            return True
    return False


#####宝箱の場所決めなど#####
dead_end_points=[i for i in range(num_of_blocks**2) if rowcol_flatten.count(i)==1 and isconnected(i)]
#スタート地点にもゴール地点にも近くない場所の行き止まりのみに宝箱を設置します
valid_dead_end_points=[point for point in dead_end_points \
    if (num_of_blocks//3<=reverse_index_for_dijkstra(point)[0]<2*num_of_blocks//3 or num_of_blocks//3<=reverse_index_for_dijkstra(point)[1]<2*num_of_blocks//3) \
    and (not (num_of_blocks//3<=reverse_index_for_dijkstra(point)[0]<2*num_of_blocks//3 and num_of_blocks//3<=reverse_index_for_dijkstra(point)[1]<2*num_of_blocks//3))]    

try:
    treasure_chest=random.choice(valid_dead_end_points)
except IndexError:
    treasure_chest=random.choice(dead_end_points)
    print("WARNING!!! This is not desirable. You chose an invalid dead end point as treasure chest location")

print("treasure_chest",reverse_index_for_dijkstra(treasure_chest))
before_treasure_chest=rowcol_flatten[rowcol_flatten.index(treasure_chest)+1] if rowcol_flatten.index(treasure_chest)%2==0 else rowcol_flatten[rowcol_flatten.index(treasure_chest)-1]
print("before",reverse_index_for_dijkstra(before_treasure_chest))
direction=reverse_index_for_dijkstra(before_treasure_chest-treasure_chest,True)
print("direction",direction)
treasure_chest_angle=direction[1]*(2-direction[0])*math.pi/4 if direction[1]!=0 else (1-direction[0])*math.pi/2
#print("treasure_chest_angle",treasure_chest_angle)
treasure_x,treasure_y=reverse_index_for_dijkstra(treasure_chest)
treasure_chest_data=[((-(num_of_blocks-1)//2+treasure_x)*scale_factor,(-(num_of_blocks-1)//2+treasure_y)*scale_factor,height[treasure_y][treasure_x],treasure_chest_angle)]

#print("distance-->\n",distance,"\n")
print("X:0 Y:0-->",distance_mid[0])
print("X:0 Y:{}-->".format(num_of_blocks-1),distance_mid[1])
print("X:{} Y:0-->".format(num_of_blocks-1),distance_mid[2])
print("X:{0} Y:{0}-->".format(num_of_blocks-1),distance_mid[3])


#####(Unityで使うための)データを保存#####
dt_now=str(datetime.datetime.now()).replace(":","_").replace("-","_").replace(".","") #ファイル名として使えない文字などを取り除く
dt_now=dt_now[-11:-6] #最終的に用いるには日時だと長すぎるので、分秒だけを取り出します。

def conversion_for_unity(mydata): #Unityの形式に合うようデータを整形します。
    return [[x,y,z-foundation_height-entire_cube_sidelength/2,(180*angle/math.pi)] for x,y,z,angle in mydata]
def write_txt_file(name:str,mylist:list): #C#の形式に合うようデータを出力します。
    leny=len(mylist)
    txt_file.write("\tpublic static readonly float[,] {0} = \n".format(name+"_side_"+dt_now))
    txt_file.write("\t{\n")
    temp_txt_list=["\t\t{"+"f,".join([str(n) for n in mylist[i]])+"f}," for i in range(leny)] #fはfloatへのキャスト
    txt_file.write("\n".join(temp_txt_list)+"\n")
    txt_file.write("\t};\n\n")

height_for_txt=[[height[j][i]-foundation_height-entire_cube_sidelength/2 for i in range(num_of_blocks)]for j in range(num_of_blocks)]
dif0_for_txt=conversion_for_unity(dif0)
dif1_for_txt=conversion_for_unity(dif1)
dif1_diag_for_txt=conversion_for_unity(dif1_diag)
dif2_for_txt=conversion_for_unity(dif2)
treasure_chest_for_txt=conversion_for_unity(treasure_chest_data)
distance_for_txt=[[[distance[k][index_for_dijkstra(i,j)] for i in range(num_of_blocks)] for j in range(num_of_blocks)] for k in range(4)]

#1048576==2**20 C#はint型にinfがないそうなので、この数で代用しました。桁あふれが怖いので、少し小さめです。
distance_for_txt=[[[distance_for_txt[k][j][i] if distance_for_txt[k][j][i]!=float('inf') else 1048576 for i in range(num_of_blocks)] for j in range(num_of_blocks)] for k in range(4)]
with open("C:\\Users\\hari64\\OneDrive\\ドキュメント\\Blender\\blender script\\"+"maze_data_"+dt_now+".txt","x") as txt_file: #txtを日付付きで新規作成
    write_txt_file("height",height_for_txt)
    write_txt_file("dif0",dif0_for_txt) #unityとblenderで軸などが異なりますが、ここでは数値を変換せずblenderの値をそのまま出力します。
    write_txt_file("dif1",dif1_for_txt) #ちなみに書いておくと、blenderでのz軸がunityでのy軸になります。
    write_txt_file("dif1_diag",dif1_diag_for_txt)
    write_txt_file("dif2",dif2_for_txt)
    write_txt_file("treasure_chest",treasure_chest_for_txt)

with open("C:\\Users\\hari64\\OneDrive\\ドキュメント\\Blender\\blender script\\"+"maze_dijkstra_"+dt_now+".txt","x") as txt_file:
    txt_file.write("\tint[,,] distance_side_"+dt_now+" = new int[4,{0},{0}]\n".format(num_of_blocks))
    txt_file.write("\t{\n")
    for k in range(4):
        txt_file.write("\t\t{\n")
        for j in range(num_of_blocks): #intのためfは不要
            txt_file.write("\t\t\t{"+",".join(map(lambda x: str(int(x)), distance_for_txt[k][j]))+"},\n")
        txt_file.write("\t\t},\n")
    txt_file.write("\t};\n\n")


########################################################################
#ここからbpyで実際にblender上へオブジェクトを配置していきます


#####全削除#####
for COLLECTION in bpy.context.scene.collection.children:
    if COLLECTION.name=="DONT DELETE":
        continue
    bpy.context.scene.collection.children.unlink(COLLECTION)
#for item in bpy.context.scene.collection.objects: #Scene Collectionに直接属しているオブジェクトを削除
#    bpy.context.scene.collection.objects.unlink(item)
#今回は最後までそれが発生しなかったのでコメントアウトしたままです
#for item in bpy.data.objects: #これだとDONT DELETE内のオブジェクトもすべて削除されてしまいます
#    bpy.data.objects.remove(item) #全削除コマンドとしてこれが一番有名な気がしますが、今回は使えません
for _ in range(6): #6回繰り返しているのはpurgeがネスト内のものに対して有効に働かないからです
#system consoleを見る限り、恐らく今回は5回でも大丈夫ですが、たとえ回数が多くとも
#Info: No orphaned data-blocks to purgeを吐くだけなので問題はありません。
#今回はemptyからcollection instanceを作成しているので回数が増えています。
    bpy.ops.outliner.orphans_purge() #orphansを消去しないと、命名などの邪魔になります。

#####originial collectionの作成#####
original_collection = bpy.data.collections.new("ORIGINAL"+dt_now)
bpy.context.scene.collection.children.link(original_collection)
original_collection = bpy.context.view_layer.layer_collection.children[original_collection.name]
bpy.context.view_layer.active_layer_collection = original_collection 

for x in range(-(num_of_blocks-1)//2,(num_of_blocks-1)//2+1):
    for y in range(-(num_of_blocks-1)//2,(num_of_blocks-1)//2+1):
        bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0.5))
        bpy.ops.transform.resize(value=(cuboid_sidelength, cuboid_sidelength, 1))
        bpy.ops.transform.translate(value=(region*x ,region*y , 0))
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        bpy.ops.transform.resize(value=(1, 1, height[y+(num_of_blocks-1)//2][x+(num_of_blocks-1)//2]))

bpy.data.collections["DONT DELETE"].hide_select=True #一部を選択させない
bpy.ops.object.select_all(action='SELECT') #当たり前ですが、select allを書くときは本当に全てを選択してよいのか確かめましょう。
bpy.ops.transform.translate(value=(0, 0, -foundation_height-entire_cube_sidelength/2))
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
bpy.ops.object.select_all(action='DESELECT') #そしてdeselectもお忘れなく。私は二つとも失念して惨敗しました。
bpy.data.collections["DONT DELETE"].hide_select=False


#途中まで利用していました。
#####instance collectionの作成#####
if not isfinal:
    instance_collection = bpy.data.collections.new("INSTANCE")
    bpy.context.scene.collection.children.link(instance_collection)
    instance_collection = bpy.context.view_layer.layer_collection.children[instance_collection.name]
    bpy.context.view_layer.active_layer_collection = instance_collection 

    for i in range(5):
        bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
        bpy.ops.transform.rotate(value=math.pi/2 if i!=4 else math.pi, orient_axis='X')
        bpy.ops.transform.rotate(value=i*math.pi/2 if i!=4 else 0, orient_axis='Z')
        bpy.context.object.instance_type = 'COLLECTION' #emptyのインスタンス機能を使う
        bpy.context.object.instance_collection = bpy.data.collections[original_collection.name] #インスタンスコレクションとしてORIGINALを選択
    bpy.ops.object.select_all(action='DESELECT')



#####橋や階段の建設#####
def construction(target_object:str,dif_data):
    bpy.context.view_layer.objects.active = bpy.data.objects[target_object]
    bpy.data.collections[original_collection.name].objects.link(bpy.context.view_layer.objects.active)
    bpy.data.collections['DONT DELETE'].objects.unlink(bpy.context.view_layer.objects.active)
    for x,y,z,angle in dif_data:
        bpy.context.view_layer.objects.active = bpy.data.objects[target_object]
        bpy.context.view_layer.objects.active.select_set(True)
        bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked":True, "mode":'TRANSLATION'},TRANSFORM_OT_translate={"value":(x,y,z-foundation_height-entire_cube_sidelength/2)})
        bpy.ops.transform.rotate(value=angle, orient_axis='Z',constraint_axis=(False,False,True))
        bpy.ops.object.select_all(action='DESELECT')
    bpy.context.view_layer.objects.active = bpy.data.objects[target_object]
    bpy.data.collections['DONT DELETE'].objects.link(bpy.context.view_layer.objects.active)
    bpy.data.collections[original_collection.name].objects.unlink(bpy.context.view_layer.objects.active)

construction("BRIDGE",dif0)
construction("STAIRS",dif1)
construction("DIAGONAL STAIRS",dif1_diag)
construction("SLOPE",dif2)
construction("TREASURE CHEST",treasure_chest_data)


#####FBX(3DCG用のファイル形式)のエクスポート#####
if isfinal: #これを実行するとBlocksの回転がblender上ではおかしくなります
    bpy.context.view_layer.objects.active = bpy.data.objects['Cube']
    bpy.context.view_layer.objects.active.select_set(True)
    bpy.context.view_layer.objects.active.name="Blocks_"+dt_now
    for i in range(num_of_blocks**2-1): #添え字がついていないものが必ず一つできるので、それを除くための-1
        bpy.data.objects['Cube.{:0=3}'.format(i+1)].select_set(True)
    bpy.ops.object.join() #扱いやすいように結合しておく
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    bpy.ops.transform.rotate(value=math.pi, orient_axis='Z',constraint_axis=(False,False,True))
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
    bpy.ops.export_scene.fbx(filepath='C:\\Users\\hari64\\OneDrive\\ドキュメント\\Blender\\blender script\\FBX_side_Blocks_'+dt_now+'.fbx', use_selection=True, bake_space_transform=True, object_types={'MESH'})
    bpy.ops.transform.rotate(value=-math.pi, orient_axis='Z',constraint_axis=(False,False,True))

また、このコードを書くにあたり一部友人から助言をもらいました。indexが範囲内かどうかの判定辺りです。この友人の勧めから競プロを始めましたし、この友人のコードからscipyの存在を知りました。頭が上がりません。感謝。

(最後に蛇足を。このコードは頑なにformat文を使用していて変だと感じた方もいるかも知れませんが、なんと私、f文字列、恥ずかしながらつい先日知りました。。。
特にprint(f"{x=}")とかデバッグに超便利ですよね。これで変数名と変数の値の両方が埋め込まれるそうです。f文字列自体は3.6からあるものの、この=に関する拡張は3.8からと比較的新しいので、多分一人ぐらいの読者には有益になる(のかな……?))

以上、“最終的に使用したコード全文”のプルダウンでした。

おまけ(blenderを使っている人向け)

おまけ その1

もしかしたら知っている方も多いかも知れませんが、プロパティシェルフ(nキーで出てくるもの)→View(3番目)→View(先頭)→Focal Lengthで、画角を変えられます。私はここを滅多に触らないので見づらいのを我慢しながら途中まで作業していました。こういう「広角で全体をちゃんと見たい!」、という時にとても便利ですね。またWalk Navigationでwasd操作が出来るので、お手軽ゲーム体験ができます。こっちもUnityにわざわざ持って行かなくともゲームの雰囲気が分かるので便利です。他にも私が知らなそうなマイナーな豆知識をご存じでしたら教えてもらえると嬉しいです。

おまけ その2

bpy.ops.transform.rotate(value=1.5708, orient_axis='Z', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, True), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False)

がR→Z→90と打った時、つまり、z軸に90度回転した時のInfo欄の表示なんですが、これでは冗長です。なので、関数の引数を省略すればデフォルト値が使われることを利用して

bpy.ops.transform.rotate(value=1.5708, orient_axis='Z', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)))

と、大事そうかなと思う部分だけを残して他の部分を消して普段私はコードを書いていました。(過去形)
しかし、なんとびっくり、このコードだと-90度回転になります。マジか。
私は全くこのことを知りませんでした。今回の作業中にも何か角度が合わないなという時は計算ミスかと思っていましたが、どうやらそもそもコードが間違っていたようです。
本当に必要なのは

bpy.ops.transform.rotate(value=1.5708,constraint_axis=(False, False, True))

とconstraint_axisでした。これを消してしまうと意図しない動作をするようです。(※orient_axisがzでない場合などは他の要素も必要になります。)ちゃんと公式のドキュメント見て何がデフォルト値なのか気を付ける必要がありますね。。。
(追記 しかもなにより厄介なのは180度回転をしようとしてmath.piとかを代入すると逆にマイナスの値が出る可能性があることです。value=1.57とかだと普通に角度が加算されていくのですが、180度回転などだとお節介機能でmodが入るようです。(つまり、-180d→0d→-180d→0dを繰り返していく) とにかく大切なのは、怪しいと感じた段階で何度回転させられたのか愚直に確認していくことだと思います。仕様もいつ変わるか分からないので)

おまけ その3

SciPy等の外部ライブラリのimportに関して、blender上では一部ライブラリに関しては別途導入が必要なようです。場合によっては詰まると思います。というか、私がとても詰まりました。

(環境)Blender 2.90(windows 10 恐らく他も大丈夫)

random,sys,os,numpy等に関してはBlender内蔵のpythonの方にも入っているようですが、一部のライブラリは手動で入れる必要があります。import scipy等を打ってもエラーが出てきてしまうので。
検索すれば多くの情報が出てきますが、自分は半日以上詰まった上に日本語での新しい情報が少なく感じたので、2021年3月8日現在の情報を記します。(とはいえリンクペタペタだけですが)

https://rikoubou.hatenablog.com/entry/2018/11/07/195844
最初にこちらのサイトの方法などを試すことをお勧めします。しかし、特にBlender内蔵pythonのバージョンや、使いたいライブラリのバージョンなど間での齟齬が特殊(?)な場合、単なるアップデートでは済まないかも知れません。(正確には知りません。)
その場合anacondaで専用の環境を作ることで解決する可能性があります。

https://blender.stackexchange.com/questions/41258/install-python-module-for-blender
基本的に上記のWEBサイトの内容に沿えば可能です。リンク先が死んだ時の為に軽く書くと、以下をコマンドプロンプトで実行していく感じです。

Create an specific environment for the modules you need in blender:
conda create --name conda-python-blender python=3.6.0
Activate this environment:
source activate conda-python-blender
Install all your required libraries ("modules"):
conda install cython

Blender2.90ではpythonのバージョンは3.7.7です。これはBlenderのpython consoleから確認可能です。(一行目に書いてあります)
また、echoの文は効かない可能性があります。ただ、ファイルの場所を探せばパスは普通に見つかると思います。

最後にpython consoleで
>>>import sys
>>>sys.path.append('~~~')
を打つ必要がありますが、unicode errorが出た時は、
>>>sys.path
から確認できるpathの書き方の規則に沿えば大丈夫だと思います。(バックスラッシュ二個とか。)

この方法の欠点としては導入自体がやや面倒くさいのと、わざわざ環境を新しく作っているので少し(今回は100MB程度)PCの容量を食う点と、パスは毎回リセットされるので、blenderを開くたびにsys.path.appendをする必要があることです。より簡潔な方法で済みそうならばそちらをお勧めします。これはある意味奥の手かも知れません。

おまけ その4

select_all関連の惨敗(コードのコメントにも書いたもの)の様子
messageImage_1615098205998.jpg
この無法地帯感、さては世紀末?

おまけ その5

わざわざここまで読んで頂いているあなたに、超有益(※なお個人の感想)情報をお一つ。

import bpy
for COLLECTION in bpy.context.scene.collection.children:
    if COLLECTION.name=="DONT DELETE":
        continue
    bpy.context.scene.collection.children.unlink(COLLECTION)
for _ in range(6):
    bpy.ops.outliner.orphans_purge()

この7行は(最善策ではないと思うものの、)個人的に超お勧めです。これは一種の全削除のコマンドです。ここを書くのにとても苦労しました。(というか、削除を書くの難しすぎませんか…?)多くのblenderでscriptを使う方が、何かしら全削除系のコマンドを冒頭に付けているかと思います。これは何回もコードを実行するときに、前の実行結果をチャラにして元の状態に戻してくれるからです。
ただ、簡単な全削除のみだと、collectionがそのまま残ったり、(unlinkしているだけなので)orphan dataが残って命名の邪魔をしたりと不都合が多いです。それを解決してくれたのが、この7行です。さらに全削除の例外コレクションも置いておくことが出来ます。便利!(DONT DELETEという名前のコレクションにすれば、それが例外になります。)(詳しくは全文のコメントを参照してください。また、よりよい手段をご存じであればご教授ください。)

以上、”おまけ(blenderを使っている人向け)”のプルダウンでした。

Step3 Unity上でゲームを完成させる。

ゲームとして完成させていきます。迷路の生成以外にもいろいろやりましたが、ここに書いてしまってはネタバレなので、大半は省略します。ただ、一点だけマテリアルについて軽く触れようと思います。

MatCapについて

今回、マテリアルとしてはMatCapと言う技術を採用しました。
私が3DCGの技術で何が一番好きかと言われたら多分MatCapを挙げると思います。結構面白い技術です。
そもそもMatCapとは、という話ですが、Blenderのマニュアルでは次のようになっています。

Stands for “material capture”, using an image to represent a complete material including lighting and reflections.
(MatCapとは「マテリアル・キャプチャー」の略で、照明や反射の情報を含む完璧なマテリアルを画像で表現することである。(筆者訳))

以下が一例です。
image.png
↑このような画像群だけから
↓このような結果が得られます。
image.png
特に下段中央の色が派手な猿を上の画像と見比べてもらえると、雰囲気がつかみやすいかも知れません。

ここまで書くと、「なるほど、で何が凄いの?」と思われる方も多いかも知れません。世間一般的にMatCapの長所は時間計算量も空間計算量もどちらも非常に小さいということが言われています。なにせ光線の計算も何もせずに、ほぼ面の法線だけで色を決めているので、どの角度の面にどの色を振るかを決める写真一枚だけでほぼ計算は完結しています。しかも画像の数だけ結果が変わるので千変万化です。
そして今回の場合では一切のライトなしにそれらしい絵が完成するというのも長所になります。あそこにもライトを設置してこちらにもライトを設置して……、とすると色々大変なことも多いのですが、その手間も省けるのは魅力的です。
MatCapはメジャーな、しかもかなり古い技術ですが、それでもやはり凄いなとしみじみ感心します。CGに興味のあまりないプログラマの方にも、MatCapの良さが伝われば。

おまけ

おまけ その1

先述の通り、私はMatCapがかなり好きです。ただ、正直今回のようなシーンにそれを用いるのが最適かと問われると否な気がします。なにせ法線が同じ向きを向いている面が多すぎて、多くの面が同じ色になってしまい、のっぺりとした印象しか与えられません。それでもMatCapを用いているのは、プレハブをインスタンス化しているが為にライト情報を焼くに焼けないなどという消極的な理由がありました。

そしてやや残念なことに、どうやらUnityのMatCapはblenderのMatCapと異なり同一面上の色が一色しかないようです。Blenderは恐らくある程度広い範囲の情報を計算に用いているのでMatcapでもかなりいい感じの仕上がりになります。(下図参照)
messageImage_1616251127756.jpg

この点をどう解決するかはかなり悩んだのですが、結局shader graphで補正をかけるような形に着地しました。あまり最善とは思っていません。

messageImage_1616335732032.jpg
まぁ、グラフィックスに関しては次作る作品で凝れたらなと思います。
もしなにか良い方法をご存じの方がいらっしゃれば、ご教授いただけると幸いです。

おまけ その2

Step2で作成したデータをどのように使ったかは示した方が良いかと思ったので、コードを一部書いておきます。

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

static class Constants
{
    public static readonly float[,] height_side_23_53 =
    {
        {-40.5f,-42.5f,-48.5f,-52.5f,-46.5f,-50.5f,-48.5f,-42.5f,-40.5f},
        {-44.5f,-48.5f,-54.5f,-58.5f,-56.5f,-56.5f,-50.5f,-48.5f,-44.5f},
        {-48.5f,-52.5f,-54.5f,-58.5f,-62.5f,-60.5f,-52.5f,-50.5f,-44.5f},
        {-50.5f,-56.5f,-58.5f,-64.5f,-60.5f,-62.5f,-60.5f,-54.5f,-52.5f},
        {-54.5f,-54.5f,-60.5f,-60.5f,-64.5f,-60.5f,-58.5f,-52.5f,-52.5f},
        {-50.5f,-56.5f,-60.5f,-60.5f,-62.5f,-62.5f,-60.5f,-54.5f,-52.5f},
        {-50.5f,-52.5f,-54.5f,-58.5f,-60.5f,-60.5f,-56.5f,-50.5f,-46.5f},
        {-44.5f,-48.5f,-54.5f,-52.5f,-54.5f,-58.5f,-54.5f,-44.5f,-44.5f},
        {-40.5f,-42.5f,-50.5f,-52.5f,-52.5f,-52.5f,-46.5f,-42.5f,-40.5f},
    };
        //中略 データが千行程
}

public class Maze_game_manager : MonoBehaviour
{
    public GameObject prefab_BRIDGE;
    public GameObject prefab_SLOPE;
    public GameObject prefab_STAIRS;
    public GameObject prefab_DIAGONAL_STAIRS;
    public GameObject prefab_TREASURE_CHEST;

    public GameObject[] list_of_prefab_BLOCKS = new GameObject[8];

    public GameObject[] list_of_empty_side = new GameObject[4];
    public GameObject[] list_of_empty_tobo = new GameObject[2]; //tobo-->top and bottom

    void Construction(GameObject prefab, float[,] mydata, GameObject parent)
    {
        for (int i = 0; i < mydata.GetLength(0); i++)
        {
            float x = mydata[i, 0];
            float y = mydata[i, 2]; //blenderでのz軸 blenderは右手座標系 unityは左手座標系です
            float z = mydata[i, 1]; //blenderでのy軸
            float degree = mydata[i, 3]; //blenderでのz軸回転 
            //右手座標系におけるz軸中心の正方向回転は左手座標系におけるy軸中心の負方向回転
            Instantiate(prefab, new Vector3(x,y,z), Quaternion.Euler(0,-degree,0), parent.transform);
        }
    }

    void Start()
    { 
        List<int> numbers = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7 };
        numbers = numbers.OrderBy(a => Guid.NewGuid()).ToList(); //使用されるデータに重複があってほしくないのでシャッフルの方式をとりました

        System.Random random = new System.Random();

        //bottom
        Debug.Log($"bottom number:{numbers[0]}");
        Instantiate(list_of_prefab_BLOCKS[numbers[0]], new Vector3(0, 0, 0), Quaternion.Euler(0, 0, 0),list_of_empty_tobo[0].transform);
        Construction(prefab_BRIDGE,          Constants.dif0s          [numbers[0]], list_of_empty_tobo[0]);
        Construction(prefab_STAIRS,          Constants.dif1s          [numbers[0]], list_of_empty_tobo[0]);
        Construction(prefab_DIAGONAL_STAIRS, Constants.dif1_diags     [numbers[0]], list_of_empty_tobo[0]);
        Construction(prefab_SLOPE,           Constants.dif2s          [numbers[0]], list_of_empty_tobo[0]);
        Construction(prefab_TREASURE_CHEST,  Constants.treasure_chests[numbers[0]], list_of_empty_tobo[0]); 
        list_of_empty_tobo[0].transform.rotation = Quaternion.Euler(0, 0, 0); //game startしてすぐ崖は望ましくないのでランダム回転はさせない

        //side
        for (int i = 1; i < 5; i++)
        {
            Debug.Log($"side number:{numbers[i]}");
            Instantiate(list_of_prefab_BLOCKS[numbers[i]], new Vector3(0, 0, 0), Quaternion.Euler(0, 0, 0), list_of_empty_side[i-1].transform);
            Construction(prefab_BRIDGE,          Constants.dif0s          [numbers[i]], list_of_empty_side[i-1]);
            Construction(prefab_STAIRS,          Constants.dif1s          [numbers[i]], list_of_empty_side[i-1]);
            Construction(prefab_DIAGONAL_STAIRS, Constants.dif1_diags     [numbers[i]], list_of_empty_side[i-1]);
            Construction(prefab_SLOPE,           Constants.dif2s          [numbers[i]], list_of_empty_side[i-1]);
            Construction(prefab_TREASURE_CHEST,  Constants.treasure_chests[numbers[i]], list_of_empty_side[i-1]);
            list_of_empty_side[i-1].transform.rotation=Quaternion.Euler(90, (i-1) * 90, 0); //sideの四面それぞれに、当該オブジェクトを振り当てる
            list_of_empty_side[i-1].transform.Rotate(Vector3.up, random.Next(0, 4) * 90);     //ランダマイズの為の回転
        }

        //top
        Debug.Log($"top number:{numbers[5]}");
        Instantiate(list_of_prefab_BLOCKS[numbers[5]], new Vector3(0, 0, 0), Quaternion.Euler(0, 0, 0), list_of_empty_tobo[1].transform);
        Construction(prefab_BRIDGE,          Constants.dif0s          [numbers[5]], list_of_empty_tobo[1]);
        Construction(prefab_STAIRS,          Constants.dif1s          [numbers[5]], list_of_empty_tobo[1]);
        Construction(prefab_DIAGONAL_STAIRS, Constants.dif1_diags     [numbers[5]], list_of_empty_tobo[1]);
        Construction(prefab_SLOPE,           Constants.dif2s          [numbers[5]], list_of_empty_tobo[1]);
        Construction(prefab_TREASURE_CHEST,  Constants.treasure_chests[numbers[5]], list_of_empty_tobo[1]);
        list_of_empty_tobo[1].transform.rotation = Quaternion.Euler(180, random.Next(0, 4) * 90, 0);
    }
}

データ千行というのはBlenderで出力したtxtファイルの内容をコピペで済むのでただのこけおどしですが、特にきつかったのは座標系違いですね。右手座標系の方が私は好きです。

おまけ その3

最後に、UnityStandardAssetsのFirstPersonControllerについて。
私的にはこの一文の解釈にとても詰まりました。

m_CollisionFlags = m_CharacterController.Move(m_MoveDir * Time.fixedDeltaTime);

これ、ただ左辺に右辺の情報を代入しているように見えるじゃないですか。(そう見えますよね……? 私が馬鹿なだけ……?)でも実は、右辺でキャラを動かした上で、そのメソッドの返り値を左辺に代入するという二つのことをしています。ずるい。
尤も、公式リファレンス見れば分かることではありますが。

この辺りが特にきつかったです。ただ、こういう特殊なことをする文もあるんだなと頭の片隅に入れておけば心構えが出来そうです。私はすぐ忘れる気がしますが。

以上”おまけ”のプルダウンでした。

最後に

以上でゲームは完成となります。是非遊んで下さい。この記事で語ったことは半分程度です。残り半分はゲームを通して想像していただければ。迷路だけが売りではありません。

身の上話で恐縮ですが、私は今までしてきたゲームと言うのが片手で数えられるほどにはゲームに対して興味がない人間でした。ただ、絵や3DCG、そして数学(≒プログラミング)に時間を捧げてきた自分には、割とあっている趣味かなと最近は思っています。作っていて非常に楽しかったです。

私はゲームの定石(なんならWASD操作すらおぼろげでしたが……)をあまり知らない為、至らぬ点も多々あるかも知れません。ただ、おまけまで見てくれた方には分かると思いますが、blenderを扱う上での地雷と、unityを扱う上での地雷と、blender→unityへの翻訳作業における地雷という三つの地雷を踏み抜きに踏み抜きまくった3週間の産物がこの作品です。途中で心がボキッと折れたこともありました。遊んでもらえると報われます。

最後までお読みいただきありがとうございました。

参考文献、リンク先

穴掘り法など:https://algoful.com/Archive/Algorithm/MazeDig#:~:text=%E7%A9%B4%E6%8E%98%E3%82%8A%E6%B3%95%E3%81%AF%E6%96%87%E5%AD%97%E9%80%9A%E3%82%8A,%E5%AF%BE%E7%85%A7%E7%9A%84%E3%81%AA%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%A7%E3%81%99%E3%80%82

ゲームのリンク先:
https://unityroom.com/games/hari_kagiyanomusume_maze

WASDで操作、Spaceでジャンプ、Tでタイトル画面に戻ります。PCからお遊びください。
※※注意※※
本ゲームはマウスカーソルを消した状態で遊んでもらうことを前提としています。本来自動で消えるはずですが、場合によっては画面を一度クリックして頂く必要があります。挙動がおかしいと思った方は一度お試しください。

また、バグがあれば報告して頂けると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む