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

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

サーバーマッチングしてネット対戦する

サーバーからランダムマッチングできるようになった。

http://27.120.88.154/rts9.gif

Matchingボタンを押すとサーバーからランダムに対戦相手を取得する
Battleボタンで戦闘開始

マッチング後にユニットを増やしているが、これはできないようにした方がいいだろう。
クラクラの場合、マッチング後タイマーがあって、タイマーが来ると自動的に対戦開始になる。
あと、マッチングするたびにゴールドを消費する仕組みになっている。


マッチングは一覧から選ぶやりかたもあるが、それだと、別アカウントでつくった弱い相手を使って、不当に経験値を上げるといった恐れがでてくる
よって、クラクラはランダムマッチングしかないのだと思う。
ソーシャルゲームの農園ゲームなんかでは、別ユーザーの農園を代わりに収穫する機能があったりするが、放置農園を植民地にすることで、
どんどん収穫量を増やすとったことができてしまう問題点があった。
クラクラの場合、別アカウントを使うことで、援軍をつねに確保するといったチートはできてしまう。本来複数アカウントは禁止なんでしょうけど。


MySQLでのランダムレコード取得

SELECT s.* FROM data AS s INNER JOIN(SELECT CEIL(RAND() * (SELECT MAX(`id`) FROM data)) AS `id`) AS `tmp` ON s.id = tmp.id;

参考
qiita.com

MySQL: レコードがすでにあればUpdate。なければInsert

マップ配置データをサーバーに保存するにあたり、マップデータがすでにアップロードされていればUpdateし、新規であればinsertする処理を作りたい。


MySQLであればon duplicate key update機能を使う

INSERT INTO data (userid,data,datetime) VALUES(".$db->quote($userid).",".$db->quote($data).",NOW()) ON DUPLICATE KEY UPDATE userid=".$db->quote($userid).",data=".$db->quote($data).",datetime=NOW();"

同じuseridのレコードがあれば、dataとdatetimeのupdateが行われ、新規の場合は、Insertが行われる。

なお、重複チェックの対象であるuseridは、UNIQUEインデックス(かPRIMARY KEY)を指定しておく必要がある。
要するに、同じuseridのレコードは一つしかないという状態になっている。

同様な処理をするのに、以前は、まずselectしてデータがなければInsert。あればUpdateみたいなことをやっていたが、SQL発行が一回で済んで楽。


参考
qiita.com

ユニークIDの保存

サーバーに配置データを保存するにあたり、ユーザーを区別するためにユニークIDを発行しなければならない。
また、ユニークIDはローカルに保存しておいて、アプリを起動するたびに同じIDが読み込まれるようにする必要がある。



データをセーブするには、以前の記事に書いたSaveData.csを使う。

yasu9780.hatenablog.com



セーブデータは以下のような構造とする。

    class Player
    {
        public string uuid;
        public int mode;
        public Vector3[] pos;
        public int[] type;
        public Player()
        {
            uuid = "";
            mode = 0;
            pos = new Vector3[100*3];
            type = new int[100 * 3];
            for (int i = 0; i < 100 * 3; i++) type[i] = -1;
        }
    }
    Player player;

ユニークIDはuuidに保存されている。


起動時に、以下のような処理を入れる

   void Start () {

        player = SaveData.GetClass<Player>("p1", new Player());
        if(player.uuid=="")
        {
            System.Guid guid = System.Guid.NewGuid();
            player.uuid = guid.ToString();

            SaveData.SetClass<Player>("p1", player);
            SaveData.Save();
        }
        Debug.Log("uuid=" + player.uuid);

p1というキーで保存されているデータを読み込む。
初回はデータが無いので、new Player()の結果が返ってくる。
コンストラクターがuuidに""に代入しているので、初回はuuidが空として、uuid発行処理が走る。UUIDを発行したので、p1をキーとしてplayerをセーブする。

一度アプリを終了させ、次回の起動では、SaveData.GetClassの読み込みでuuidが入っているので、uuid発行処理は今度は走らない。

これで、初回時はuuid発行。以後は同じuuidが起動時にセットされるという処理ができた。

キャラ配置をVPSサーバーに保存・読み込み成功

キャラを配置して、Saveボタンを押すと、キャラ配置データをCSVにして、HTTPのGETでVPSサーバーのCGIに転送
LOADボタンを押すと、VPSサーバーのCGIからデータを取得して、キャラ配置を再現。
f:id:yasu9780:20170324194944p:plain


phpMyAdminで確認すると、MySQLに保存したCSVデータが入っているレコードができている。

f:id:yasu9780:20170324194950p:plain

テスト用で、useridは1になっているが、スマホ上のユニークIDをユーザーIDとして実行すれば、
ユニークIDごとに配置データをVPS上に保存できる。

後は、別のスマホから保存した配置データをLOADできるようにすれば、違うユーザー同士で対戦が可能になる。



回転向きは固定なので保存する必要なし。本来はY座標も固定なんですが、いちおう保存します。typeはキャラ種別。

        if(no == 2 && !battle) // save
        {
            string temp = "";
            for (int i = 0; i < 100; i++)
            {
                if (ch[i] == null) continue;
                temp += chcc[i].type + ",";
                temp += ch[i].transform.position.x.ToString("0.0") + ",";
                temp += ch[i].transform.position.y.ToString("0.0") + ",";
                temp += ch[i].transform.position.z.ToString("0.0") + "@";
            }

            StartCoroutine(HttpGet("http://VPSURL/?u=1&d="+temp,0));
        }

魔法のエフェクト追加

http://27.120.88.154/rts7.gif
範囲攻撃は突き抜けるので、縦に並んでいる敵には有利ですね。
最後は、ダブルノックアウトw


assetStoreのこのパーティクルエフェクトを買いました。
Pinwheel Fantasy Effect
https://www.assetstore.unity3d.com/jp/#!/content/50310

ドキュメントを見ると、作者はベトナムの方でした。
30種類ぐらい入って$2ぐらいでお得でした。
パーティクルシステムも勉強しないと、ちょっといじりたいなと思ってもなかなかできないですね。

Vector3 pos = transform.position + Vector3.up*2.2f+transform.forward*1;
GameObject obj;
if (type == 3 ) // 魔法使い
{
        obj = (GameObject)Instantiate(Resources.Load("Magic"), pos, transform.rotation);
        obj.GetComponent<Magic>().power = attackPower;
        obj.GetComponent<Magic>().enemy = enemy;
        obj.GetComponent<Magic>().target = targetcc.gameObject;

        Vector3 pos2 = transform.position + Vector3.up * 0f;
        GameObject obj2;
        obj2 = (GameObject)Instantiate(Resources.Load("VenomSpell"), pos2, transform.rotation);
        Destroy(obj2, 3f);
}

Magicはトリガー付きの球です(MeshRenderが非表示)。攻撃力とターゲットを設定して生成。
緑色の魔法陣エフェクトを自分の足元で生成。Destroyは第二引数に時間を設定しておくと、指定時間後に自動削除されるので便利です。



親から、攻撃力と敵を与えられて生成される。Startで敵の方向ベクトルを作っておいて、後はひたすらその方向に移動していく。
球の大きなトリガーがついているので、触れた敵のGetDamage()を呼び出してダメージを与えます。
これが弓では何か敵に当たると、そこでデストロイしてますが、範囲攻撃の魔法はDestoryしないので、指定時間までは連続で殺傷できます。
ここでは寿命を3秒にしてますが、放っているキャラに応じて変えたい場合は、powerのように外部から値を渡せばいいです。

magic.cs

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

public class Magic : MonoBehaviour
{
    public float power;
    public GameObject target;
    public int enemy;

    Character targetcc;
    Main main;
    float timer;
    Vector3 fw;

    void Start()
    {
        main = GameObject.Find("Main").GetComponent<Main>();
        timer = 3f;
        targetcc = target.GetComponent<Character>();

        fw = (target.transform.position + Vector3.up * 2.5f - transform.position).normalized;

        Vector3 pos2 = transform.position + Vector3.up * 0f;
        GameObject obj2;
        obj2 = (GameObject)Instantiate(Resources.Load("IceSpear2"), pos2, transform.rotation,this.transform);
        Destroy(obj2, 3f);
    }

    void Update()
    {
        if (timer > 0) timer -= Time.deltaTime;
        else
        {
            Destroy(this.gameObject);
            return;
        }
        transform.position += fw * Time.deltaTime * 7;
    }
    void OnTriggerEnter(Collider other)
    {
        Character cc = other.GetComponent<Character>();
        //Debug.Log(other.name);
        if (cc != null && cc.enemy!=enemy ) cc.getDamage(power);
    }
}


Start()で、Destroy(this.gameObject,3f)としておけばupdate()でtimer更新はいらないですね。