AIプログラムとかUnityゲーム開発について

探索や学習などを活用したAI系ゲームを作りたいと思います。

ゲームデータのセーブ

PlayerPrefsと同じような使い方で独自クラスもセーブできる機能実装【Unity】【セーブ】【Json】 | Unity開発Tips
上記のライブラリをテスト中

テスト用のサンプルにデータの削除を追加して削除されるか確認

        //セーブデータ
//        SaveData.SetClass<Player> ("p1", new Player ());
//        SaveData.Save ();

        //ロードデータ
        Player getPlayer = SaveData.GetClass<Player> ("p1", new Player ());
 
        Debug.Log (getPlayer.name);
        Debug.Log (getPlayer.items.Count + "こアイテムを持ってます");
        Debug.Log (getPlayer.items[0] + getPlayer.items[1] + getPlayer.items[2] );

SaveData.Remove("p1");
SaveData.Save();

あれ削除されてない?

それもそのはず。new Player()で作られるデータは、セーブしていたデータと同一なので、
GetClassしたときに、キー"p1"のバリューは空になっているのに、次の引数であるnew Player()の結果が再代入されているのですw
Playerクラスとのコンストラクタを、最初はアイテム何も持ってないに変えれば、Removeされたデータのアイテムが空になります。
なんかちょっとわかりにくかった。


データの保存先は、
Application.persistentDataPath
に入っているパス

データ形式JSONで、確認してみると
サンプルだと、

{"keys":["i","p1"],"values":["10","{\"hp\":10,\"atk\":100.0,\"name\":\"クラウド\",\"items\":[\"ポーション\",\"エーテル\",\"エリクサー\"]}"]}

のような形式で保存されていた。

暗号化対応

暗号化の解説もあるので、どうせなら暗号化してみよう。
セーブ機能の実装方法まとめ 使い勝手の良いセーブを実装する【Unity開発】 | Unity開発Tips


動いた。Crypt.csもAsset内にダウンロードして、

SaveData.csの
Save()に
追加↓
dictJsonString = Crypt.Encrypt(dictJsonString);

Load()に
追加↓
string decrypted = Crypt.Decrypt(sr.ReadToEnd());
変更↓
var sDict = JsonUtility.FromJson>(decrypted);

これでデータの暗号化・暗号化解除ができてるみたい。

f:id:yasu9780:20170223170835p:plain

バイナリ保存対応

これって、暗号化プログラムを読むと、
暗号化:暗号化したバイナリをBase64でテキストにして保存。
復号:Bas64テキストをバイナリに変換して復号してテキスト化

これって暗号化したバイナリをわざわざテキストにして保存して、
テキストを読み込んで、またバイナリにして復号してるけど、
ならバイナリはバイナリで保存した方がファイル小さくなるんじゃないの?

am1tanaka.hatenablog.com
↑バイナリ保存方法

バリナリ保存できないか試してみる

バイナリ化できたっぽい


SaveData.csのLoad()を

public void Load() {
 if (File.Exists(path + fileName) && saveDictionary != null)
 {
    string decrypted = Crypt.DecryptB(File.ReadAllBytes(path + fileName));
    var sDict = JsonUtility.FromJson<Serialization<string, string>>(decrypted);
    sDict.OnAfterDeserialize();
    saveDictionary = sDict.ToDictionary();
 }
            else { saveDictionary = new Dictionary<string, string>(); }
}

Save()を

public void Save() {
            var serialDict = new Serialization<string, string>(saveDictionary);
            serialDict.OnBeforeSerialize();
            string dictJsonString = JsonUtility.ToJson(serialDict);
            File.WriteAllBytes(path+fileName, Crypt.EncryptB(dictJsonString));
}

Encrypt.csを、
バイナリで読んで復号テキスト返す
テキストを暗号化してバイナリ返す
の二つ関数を追加
元々バイナリをテキストにしてる部分をやめるだけ
もっとも、バイナリといっても、数値1234は文字なら4文字だけど(asciiなら4byte)、
16bitの値なら2byteで表せる。みたいな方向のバイナリ化をしないとデータが小さくなるってことはあまりないと思う。
圧縮とかすれば別だろうけど。

    public static byte[] EncryptB(string text)
    {
        RijndaelManaged aes = new RijndaelManaged();
        aes.BlockSize = 128;
        aes.KeySize = 128;
        aes.Padding = PaddingMode.Zeros;
        aes.Mode = CipherMode.CBC;
        aes.Key = System.Text.Encoding.UTF8.GetBytes(AesKey);
        aes.IV = System.Text.Encoding.UTF8.GetBytes(AesIV);

        ICryptoTransform encrypt = aes.CreateEncryptor();
        MemoryStream memoryStream = new MemoryStream();
        CryptoStream cryptStream = new CryptoStream(memoryStream, encrypt, CryptoStreamMode.Write);

        byte[] text_bytes = System.Text.Encoding.UTF8.GetBytes(text);

        cryptStream.Write(text_bytes, 0, text_bytes.Length);
        cryptStream.FlushFinalBlock();

        byte[] encrypted = memoryStream.ToArray();

        Debug.Log("encrypted:" + System.Convert.ToBase64String(encrypted));

        return encrypted;
    }

    public static string DecryptB(byte[] encrypted)
    {
        RijndaelManaged aes = new RijndaelManaged();
        aes.BlockSize = 128;
        aes.KeySize = 128;
        aes.Padding = PaddingMode.Zeros;
        aes.Mode = CipherMode.CBC;
        aes.Key = System.Text.Encoding.UTF8.GetBytes(AesKey);
        aes.IV = System.Text.Encoding.UTF8.GetBytes(AesIV);

        ICryptoTransform decryptor = aes.CreateDecryptor();

//        byte[] encrypted = System.Convert.FromBase64String(cryptText);
        byte[] planeText = new byte[encrypted.Length];

        MemoryStream memoryStream = new MemoryStream(encrypted);
        CryptoStream cryptStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);

        cryptStream.Read(planeText, 0, planeText.Length);

        Debug.Log("decrypted:" + System.Text.Encoding.UTF8.GetString(planeText));

        return (System.Text.Encoding.UTF8.GetString(planeText));
    }

UnityTexturePaintを試す

qiita.com

上記で公開されているUnityTexturePaintを試してみた。
SDユニティちゃんにペイントしている画像もあったんで、SkinnedMeshにも対応してるのかな?と思ったんですが、
DynamicCanvasの仕様はmesh前提の模様?

とりあえずmeshで試してみましたが、meshならpaintできました。

f:id:yasu9780:20170223090006p:plain
↑なぜかサンプルを実行すると右側(mesh)の顔のテクスチャがおかしくなる
 左はSkkinedMeshなのでDynamicCanvasはAddできない。



腕はもともと左右対象テクスチャなので仕方ないですが、足はちゃんと左右別に濡れました。
このアセットは、元々のテクスチャをRenderTextureに読み込んで、meshCollider上の頂点のUVを求めて、RenderTextureにペイントするという仕組みのようです。
したがって、このRenderTexureを保存すれば、ペイントしたテクスチャが得られれるし、
もしくはこのRenderTextureを、SkinnedMeshのtextureとして使用できれば、連携できるかもしれない?


http://d2ujflorbtfzji.cloudfront.net/key-image/7b2ccc3b-b374-49f9-af36-95c302238629.jpg

https://www.assetstore.unity3d.com/jp/#!/content/61022
UVPaint買えば、SkinnedMeshにも塗れて、すべて解決なんでしょうけど、$54は高い(´・ω・`)



UnityでRenderTextureをファイルに保存 | Psychic VRラボの殴り書き


http://d2ujflorbtfzji.cloudfront.net/key-image/16ec5972-dc3c-4f23-a30e-6899b9c20b47.jpg

https://www.assetstore.unity3d.com/jp/#!/content/26286
>• 新機能スキン処理メッシュのペイント

Paint in 3Dが、SkinnedMeshに対応しているらしいが、定かではない。
なんとか確認できないかなあ。
$27かあ。

カウンタックに傷がついているが、法線マップのテクスチャに落書きする仕組み。面白い。



meshが対象だけど無料アセットがあった
https://www.assetstore.unity3d.com/jp/#!/content/33506
www.youtube.com


http://codeartist.mx/tutorials/dynamic-texture-painting/
技術解説が分かりやすい

MeshColliderの頂点にRaycastして、hit.textureCoordにUV座標がすでに入っている模様。
銃の弾痕や血のりなら、raycastで十分動きそう。

bool HitTestUVPosition(ref Vector3 uvWorldPosition){
 RaycastHit hit;
 Vector3 mousePos=Input.mousePosition;
 Vector3 cursorPos = new Vector3 (mousePos.x, mousePos.y, 0.0f);
 Ray cursorRay=sceneCamera.ScreenPointToRay (cursorPos);
  if (Physics.Raycast(cursorRay,out hit,200)){
     MeshCollider meshCollider = hit.collider as MeshCollider;
   if (meshCollider == null || meshCollider.sharedMesh == null)
       return false; 
   Vector2 pixelUV = new Vector2(hit.textureCoord.x,hit.textureCoord.y);
   uvWorldPosition.x=pixelUV.x;
   uvWorldPosition.y=pixelUV.y;
   uvWorldPosition.z=0.0f;
   return true;
  }
  else{ 
   return false;
  }
 }

バスを走らせて乗せてみる

f:id:yasu9780:20170223015844p:plain

f:id:yasu9780:20170223015850p:plain

内装もあるバスの3Dデータが無料でダウンロードできたので、走らせてみました。
ただ、元がobjファイルで、Unityにはimportはできるんですが、マテリアルが無い状態。テクスチャはあるので貼ればバスの絵にはなるんですが、
窓が別マテリアルになってないので透過できません。
Blenderで読み込んで、窓の部分を選択割り当てして、窓の部分にunlit/transparentを割り当てたら透過しました。
ただし、外から中は透過してますが、中から外は透過してません。
メッシュは表と裏の張り合わせなので、内側のメッシュの窓も選択割り当てする必要があります。

ということで、外からと内からで窓が透明になりました。

まだ、回転させるために車輪のメッシュを分ける。ドアを開けるためにドアだけ別のメッシュにするといった作業が残ってますが、それはまた後日。


具体的にCharacterControllerなモデルをバスに乗せるうえで、まず飛んで屋根に飛び乗りました。バスは動いてますので、走らないと落ちますw
摩擦をつけようってことで、動き摩擦1にした物理マテリアルを貼りましたが、全然だめです(´・ω・`)
背もたれみたいにキューブを置けば、キューブに押される形で落ちなくなりましたが、それは違うでしょう。

結局、バスの子供にするしかないようです。
ただ、meshColliderとCharcterControllerが反発するので、乗せ方がづれると吹っ飛びます。
要するに、自転車に乗ってるのと同じなんですが、違うのはバスの中で移動ができるってこと。


学校に前にバスストップを作って、停車中はドアが開いていて、乗り込むと、バスが運んでくれると面白いですね。
電車なんか走ったら凄いいいかも。


f:id:yasu9780:20170223021716p:plain

大富豪ゲームでコンボランキング作成中

公開中の大富豪ゲームで連続で大富豪になるとコンボ数を更新していますが、その情報はサーバーに送信しています。
しばらく何もやってないので、連続コンボ数ランキングをやってテコ入れしようかと思います。

恒常的なランキングにすると、上位がまったく動かなくなって、ランキングの意味がなくなるので、直近三日にしようと思います。
過疎るとバレバレですが(´・ω・`)

プライバシーに配慮して日付のみにしよう。

直近の三日の連続コンボ数ランキング

8 02-20
7 02-20
7 02-20
6 02-20
6 02-20
5 02-21
5 02-20
5 02-20
5 02-19
4 02-21

SQL的には

SELECT combo,DATE_FORMAT(datetime, '%m-%d') AS datetime FROM comboRanking WHERE (DATE_ADD(datetime, INTERVAL 3 DAY) > NOW()) ORDER BY combo DESC LIMIT 10;

あとはcrontabで1日一回更新して結果をtextで保存
unityアプリからサーバーのtextを読み込んで表示

生徒数8x6=48人に増員

シューズロッカーが1ブロック8つまでだったので、1クラス8人にしました。
校舎の都合で教室は6つしかないので、6組まで。


部活の着替え用にロッカー室を作りたいけど、48個もロッカーを並べる作業はやりたくないので、
editor拡張で並べます。

あらかじめロッカーはprefabのlockerと用意しておきます。
教室や学校の周囲のフェンスも、同様にeditor拡張で配置しています。

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

public class setClassRooms : EditorWindow
{
    [MenuItem("Window/Set ClassRooms")]
    static void Init()
    {
        setClassRooms window = (setClassRooms)EditorWindow.GetWindow(typeof(setClassRooms));
        window.Show();
    }
    void OnGUI()
    {
        if (GUILayout.Button("Set Lockers")) setter4();
    }
    void setter4()
    {
        GameObject parent = GameObject.Find("lockers");
        GameObject pf = (GameObject)Resources.Load("locker");

        Quaternion rot = Quaternion.Euler(0, 0, 0);
        Quaternion rot2 = Quaternion.Euler(0, 180, 0);

        for (int y = 0; y <= 4; y++)
        {
            for (int x = 0; x <= 10; x++)
            {
                GameObject ob;
                Vector3 pos = new Vector3(x*0.6f, 0, y*1.6f);
                Vector3 pos2 = new Vector3(x*0.6f, 0, 1.6f*(y-0.7f));
                if ((y % 2) == 0)
                {
                    ob = Instantiate(pf, Vector3.zero, rot2, parent.transform);
                    ob.transform.localPosition = pos2;
                    ob.transform.localRotation = rot;
                }
                else
                {
                    ob = Instantiate(pf, Vector3.zero, rot, parent.transform);
                    ob.transform.localPosition = pos;
                    ob.transform.localRotation = rot2;
                }
                int num = (x + y * 11 + 1);
                ob.name = ob.name.Replace("(Clone)", num.ToString() );
            }
        }
    }
}

ロッカー閉じ込め事故w

テニス部の部員が着替えているときに、ロッカーの中に入って遊んでいたら、
そのままロッカーの扉を閉められて出られなくなった伊藤ともか(主人公)
このままだと空腹と尿意のパラメーターが上昇していく

f:id:yasu9780:20170221015122p:plain

部活が終わったらまたロッカーに着替えに戻ってくるから、そのタイミングで出られますw
想定外の事故だった。
こういう想定外事故が起きるような自由度の高さを追求していきたい。
ロッカーあけたら人が入ってたら驚くようなAI作りたいね。仕込みじゃなくて。


ロッカーには本体にも扉にもmeshColliderが仕込んであるので、CharacterContorollerには脱出不能。
Colliderが設定されていると反発する

CharacterController cc;
cc = GetComponent<CharacterController>();

cc.Move(transform.forward * speed * Time.deltaTime);//移動

コールバック以外にもflagでも衝突が分かる

if (cc.collisionFlags != CollisionFlags.None) //衝突中

接地判断も付属してるけど、ときどきおかしい

if (cc.isGrounded) isGround = true;

接地判断は誤動作するのでraycastも必要

if (Physics.Raycast(transform.position, Vector3.down, out hit, 10f, 1))
// Layer default only

自分から下に向けて距離10でraycast
レイヤーはdefaultのみ見る


影の表示。どうせ接地判定するので影も出してみましょう
飛行やジャンプするキャラでは高さの変化をプレイヤーに伝える意味で影は必須です
shadowは、透明度ありの影画像を貼ったplaneです。rayが衝突した場所のちょっと上に置くと、いい感じで影になります。
hitした場所が距離0.2未満なら接地とみなします。
ただし、時々浮く感じがするので、少しだけ地面に押し付けます。

if (Physics.Raycast(transform.position, Vector3.down, out hit, 10f, 1))// Layer default only
{
     shadow.transform.position = hit.point + Vector3.up * 0.15f;
     if (hit.distance < 0.2f)
     {
        isGround = true;
        cc.Move(-Vector3.up * 0.01f);// 一応押しつけしとく
     }
}

自動販売機でコーラを買えるようになった

f:id:yasu9780:20170219003531p:plain

Buy ColdDrinkを選ぶとコーラーがごろごろ転がるので
PickUpでつかむと右手にコーラを持てる

コーラはDropするか投げることができるが、まだ飲むことはできない(´・ω・`)
Swingもできるが、攻撃用のスクリプトをaddしてないので当たってもノーダメージ。
コーラで攻撃できてもそれはそれでいいかもしれない。

自動販売機のテクスチャが思いっきり米国なので、日本っぽいものに変えたい。
なお、shaderが自発光イルミネーションになっているので暗くても光っているが、
ライトではないので、ヤンデレちゃんの顔を照らすわけではないみたい。
このへんのライト関係はほとんど知識がないので解らない。