simplestarの技術ブログ

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

AIに身体性を与えるためのマイクロワールドの構築11:モデルベース駆動

モデルとしているデータ世界に更新を加えると反応して変化するビュー側世界を観測できるようにしました。

f:id:simplestar_tech:20171224195439g:plain

地味ですが、ブロックを積み上げるデータ世界の出来事が、私たちが認識できる形で表示されています。
モデルを更新するとビューが反応する、当たり前ですが、とても重要なことです。

来年からは、細かいブロックのルールを決めて、世界が育っていく様を観察できるようにしていきます。

先週は3連休を取ったのですが40℃近い熱を出して寝込んでしまって…まったく作業できませんでした。
つらかった…ではまた次の休日にお会いしましょう!

以下、実装メモです…どうやって作っているか興味ある方以外は読む必要はありません。

構築5の頃から、観測者を Unity 側に置いていましたが、これが間違いであることにやっと気づきました。

以下の処理は本当は Model 側が行わなければならなかったのです。

有効視界内に新たに入るチャンクを、自分に近いチャンクごとのキューに加えて、未読み込みのチャンクについて、チャンクファイルデータの読み込みとメッシュの構築を走らせます。
開放チャンクキューに入っているチャンクを次のアクティブチャンクの切り替え時に取り出して、その時に有効視界範囲外のチャンクになっていたら、チャンクデータを開放します。

外部から Observer が何チャンクにいるのか、これを指定する必要があります。

まずは Unity 座標系で Observer の位置からチャンクを特定する処理を UnityWorld 側に作ります。

既に作っていました。

namespace SimpleStar
{
    public class UnityWorld : SimpleMonoBehaviour
    {
        public static Chunk GetChunk(Vector3 position)
        {
            return World.Instance.ChunkArray[_RemapX(Mathf.FloorToInt(position.x / (UnityChunk.WidthOffset * Chunk.Width)))][_RemapZ(Mathf.FloorToInt(position.z / (UnityChunk.DepthOffset * Chunk.Depth)))];
        }
    }
}

この Observer がいるチャンクをモデル側の World に伝えます。

World.Instance.SetObserverChunk(UnityWorld.GetChunk(this.transform.position));

このときに、モデル側で必要なチャンクの読み込みが走るものとします。

しかし、読み込みは非常に時間を要する処理なので、非同期実行で読み込む必要があります。
これは外側から UniRX の機能を使って次のように呼ぶことで解決します。

            Vector3 observerPosition = _observer.transform.position;
            Observable.Start(() => {
                World.Instance.SetObserverChunk(GetChunk(observerPosition));
            }).Subscribe(_ => {
            });

そして、これまでビュー側で調べていた部分を、モデル側から更新通知を受け取るように修正します。
ここで、モデル側では 0~255 チャンクしかありませんが、ビュー側は -5~5 チャンクで表示するなど、observer の位置によって
チャンクの出現位置が異なることが明らかになりました。

そこで、ビューはその時のチャンクがどこに配置されるべきなのか計算する必要があります。
この計算を具体的にすると次の通りです。

        private Vector3 GetChunkObjectPosition(Chunk chunk, out int offsetX, out int offsetZ)
        {
            Vector3 observerPosition = _observer.transform.position;
            Chunk zeroChunk = GetChunk(observerPosition);

            offsetX = chunk.Longitude - zeroChunk.Longitude;
            if (World.Instance.LongitudeSize / 2 < Mathf.Abs(offsetX))
            {
                if (0 < offsetX)
                {
                    offsetX = (chunk.Longitude - World.Instance.LongitudeSize) - zeroChunk.Longitude;
                }
                else if (0 > offsetX)
                {
                    offsetX = chunk.Longitude - (zeroChunk.Longitude - World.Instance.LongitudeSize);
                }
            }

            offsetZ = chunk.Latitude - zeroChunk.Latitude;
            if (World.Instance.LatitudeSize / 2 < Mathf.Abs(offsetZ))
            {
                if (0 < offsetZ)
                {
                    offsetZ = (chunk.Latitude - World.Instance.LatitudeSize) - zeroChunk.Latitude;
                }
                else if (0 > offsetZ)
                {
                    offsetZ = chunk.Latitude - (zeroChunk.Latitude - World.Instance.LatitudeSize);
                }
            }

            float widthOffset = UnityChunk.WidthOffset * Chunk.Width;
            float depthOffset = UnityChunk.DepthOffset * Chunk.Depth;

            int observerChunkPositionX = Mathf.FloorToInt(observerPosition.x / widthOffset);
            int observerchunkPositionZ = Mathf.FloorToInt(observerPosition.z / depthOffset);

            Vector3 chunkObjectPosition = new Vector3(observerChunkPositionX + offsetX * widthOffset, 0, observerchunkPositionZ + offsetZ * depthOffset);
            return chunkObjectPosition;
        }

これで 0~255 なのに -5~5 の位置にオブジェクトが配置され…

f:id:simplestar_tech:20171203233552j:plain

そして、領域外のチャンクは対象から外さなくてはなりません。
一番外周のチャンクについて削除します。
これで移動し続けてもずっとチャンクが残るような心配はなくなりました。

あと、関係ないようですが Windows Mixed Reality に対応したゲームとするため MixedRealityToolkit-Unity を導入しました。
github.com

Input の取り方が変わります。

シーン設定にて Add the Input Manager prefab を行い、その Input Manager オブジェクトに
XboxControllerInputSource コンポーネントを追加します。
加えて今まで Input から直接値を取っていたコードを次のように修正します。

using HoloToolkit.Unity.InputModule;

namespace SimpleStar
{
    public class UnityAgent : XboxControllerHandlerBase
    {
        public override void OnXboxAxisUpdate(XboxControllerEventData eventData)
        {
                // read inputs
                float h = eventData.XboxLeftStickHorizontalAxis;
                float v = eventData.XboxLeftStickVerticalAxis;
                bool crouch = OnButton_Pressed(XboxControllerMappingTypes.XboxA, eventData);

追記:HoloToolkit が勝手に変更加えたらしく、最新版では次のコードになってました

public override void OnXboxInputUpdate(XboxControllerEventData eventData)

さてさて、いよいよモデルベース駆動で重要な部分を組んでいきます。

今モデル側で変更が入ったとき、ビュー側が反応するように作られています。
例えば定期的に World 側を更新するような次のコードを書いてみます。

        public void Test()
        {
            Chunk debugChunk = _chunkData[0][0];

            for (int i = 0; i < Chunk.Width; i++)
            {
                for (int j = 0; j < Chunk.Depth; j++)
                {
                    for (int k = 0; k < Chunk.Height; k++)
                    {
                        int blockId = debugChunk.BlockData[i][j][k];
                        int nextBlockId = GetNextBlockData(debugChunk, out debugChunk, i, j, k - 1);
                        if (0 != nextBlockId && 0 == blockId)
                        {
                            debugChunk.BlockData[i][j][k] = 1;
                            break;
                        }
                    }
                }
            }
            for (int l = 0; l < Chunk.LayerCount; l++)
            {
                debugChunk.LayerUpdated[l] = true;
            }

            if (null != _viewWorld)
            {
                _viewWorld.UpdateChunkMesh(debugChunk);
            }
        }

すると、このように定期的に世界が更新されるようになりました!

f:id:simplestar_tech:20171224194732g:plain

モデルを更新するとビューが反応する。
やっと基礎的なことが確認できるようになってきました。

PersonalライセンスのUnityはダークスキンに変えられない→無理やり変える方法がある

Unity 2018.3 からは、この記事の方法でもダークスキンに変えられなくなってしまいました。
どうします?ダークスキンに切り替えるために 毎年18万円払います?
しばらく目を痛めながら我慢してみますかぁ…

永続ライセンスから新しいUnity Proサブスクリプションへ移行して毎月 Pro ライセンス料(40%割引が適用された月額料金9,720円)を払っていたのですが(一年契約なので途中でやめられません)
昔のように Pro じゃないと使えないアセットも減って、Personal ライセンスでも十分開発が進められることを確認したので、Unity Pro ライセンスを無料の Personal ライセンスにしました。(というか、ある日勝手に Personal になってた…なるほど、ライセンスが使えるマシンは一つだけということか…どうりでモバイルPCだけ使えたわけだ、まぁそれは置いておいて)

そこで驚いたことに Personal にすると、エディタのUIがライトスキンに戻され、ダークスキンにすることができない問題を確認しました。
最初バグかと思って調べたのですが、多くの人が不満を漏らす中、これが仕様ということでした。(各種掲示板でみんな叫んでたよ)

Unity.exe ファイルの一部フラグを編集すると Personal ライセンスでもダークスキンにできるらしく、それを行うツールがいくつも出ています。

これとか
unitylist.com

こちらを試しに使ってみたところ、ダークスキンになりました。

f:id:simplestar_tech:20171210212438j:plain

御覧の通り Personal なのにダークスキンを使えています。

Unity 技術の発信者の多くは Pro ライセンスなのか、ダークスキンに変換する記事は少ないようです。

なんだか悪いことしている気がしないでもないですが(いや悪いことですよね)
しかし、自分の目を守るため、このままダークスキンで作業していきたいと思います。
(色々あって、Proライセンスで作業しています。)

年末、実家に帰ってモバイルPCで Unity を起動したら Personal じゃなくて Pro になっていた(モバイルマシンにUnity2017.3を入れたらダークスキンで始まったので、すぐに気づきました。)
Unity ID のステータスを調べたら次の画像の通り…
f:id:simplestar_tech:20171230124850p:plain
え、それは、おかしい!
確かに同じアカウントでサインインしたのに、デスクトップPCで一時期 Personal での作業を強いられていた時期があったことになる。
今は実家に帰っているから、すぐに自宅のデスクトップPCを確認できないのが悔しい…

つまりは、ライセンスはマシン一台にしか有効に機能しないということでした。
Help>Manage License ... から Return License すると、一度ライセンスをはく奪されますので、この状態で再度 デスクトップ PC で入ったらできました。

お騒がせしましたー

先週までJava知らなかった人が、初めてJava通信アプリ作るとこうなる

本記事では、Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示します。

これまで Java には触れてこなかったのですが…仕方なく使わなければならない状況となったため、このたび勉強することになりました。(トホホ…)
初めて Java を触った人間がどういう風に理解しながら通信アプリを作ったのかメモします。(Java に親しんでいる人は暖かい目で見守ってください…)

Java アプリで Hello World

まずは最もシンプルな Java アプリ実行を試してみました。

Java アプリを開発するには、JavaSDK なる JDK を開発環境にインストールしなければなりません。
ダウンロードページはこちら
Java SE - Downloads | Oracle Technology Network | Oracle

取得したインストーラを叩いて PC を再起動すれば Java 開発できるようになります。
インストール直後に案内される公式ドキュメントページはこちら
docs.oracle.com

上記ドキュメントを読んで問題なく進められるならそれでよいです。
手っ取り早く Hello World プログラムを試すならばまず、次のコードを HelloWorld.java テキストファイルに書き保存します。

public class HelloWorld {
    public static void main (String[] args) {
        System.out.println("Hello World !!");
    }
}

ファイルを置いたディレクトリをカレントにして java プログラムのコンパイルをします。
javac .\HelloWorld.java
コマンドを実行し同フォルダにコンパイルの成果物として HelloWorld.class を出力します。

それでは main 関数が書かれたクラス名を指定して、.class ファイルが置いてあるディレクトリをカレントに以下のコマンドを実行してみます。
java HelloWorld

コンソール出力に
Hello World !!
と表示されました。
最初に動作確認するのはこんな感じのコードですね。

複数の実装ファイルから構成されるアプリケーションを実行する

Java はファイルに1クラスしか記述できないようなので
複数のファイルと連携するプログラムを書くのが普通のアプリケーション開発です。

では別のクラスを用意してみましょう。
HelloWorld.java に次のコードを書き

public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }
}

Main.java に次のコードを書きます

public class Main {
    public static void main (String[] args) {
    	HelloWorld helloWorld = new HelloWorld();
    	helloWorld.printHelloWorld();
    }
}

ビルドコマンドは
javac .\Main.java
初心者として驚いたことに、勝手に依存関係を見てくれたのか一つのコマンドから HelloWorld.calss, Main.class ファイルが二つ同時に出力されました。
実行コマンドに
java Main
を叩けば
Hello World!!
と出力することを確認できます。

メインクラスから別のクラスの機能を呼び出すことができるようになりました。
さて、ここで HelloWorld.class ファイルを削除して
実行コマンド
java Main
を叩くと次のエラーメッセージが表示されます。
Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld
at Main.main(Main.java:3)
Caused by: java.lang.ClassNotFoundException: HelloWorld
このような NoClassDefFoundError が出る時は .class ファイルが配置されていないケースととらえることができます。
実行時エラーが発生した時の手掛かりとして覚えておきましょう。

さて、続いてサブフォルダを切って
ファイルを管理していきましょう。

先ほどの
HelloWorld.java に package の行を書き加えます。

package sub;

public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }
}

そして HelloWorld.java ファイルを sub フォルダを作成してその中に入れます。

Main.java に次の import 行を書き加えます

import sub.HelloWorld;

public class Main {
    public static void main (String[] args) {
    	HelloWorld helloWorld = new HelloWorld();
    	helloWorld.printHelloWorld();
    }
}

もう一度ビルドコマンド
javac .\Main.java
を叩けば、各フォルダに .class ファイルが作られ
実行コマンド
java Main
を叩くと正常に Hello World!! が表示されます。

.class ファイルがあちこちに散在するのが厄介だなと思います。

ビルド成果物を一つの jar ファイルに集約して実行する

そろそろコーディングが記憶を圧迫してくるのでインテリセンスを使います。
Java のエディタなんてのも初めて触りますが、IntelliJ IDEA で作ったJavaコードをレビューしてくれと頼まれたことが過去にあったので
これを機に IntelliJ IDEA を Java のエディタに利用してみようと思います。(その時は構造レビューなので書式詳細は見なくて良かったよ)

インストーラのダウンロードページはこちら
www.jetbrains.com

さてさて、インストーラの指示に従ってインストールしますが、ビルドツールとして Maven と Gradle があるよって示されます。
今回は Maven をビルドツールとして使っていきたいと思います。

Maven を使えるようにするにはまずツールをダウンロードして環境で利用できるようにする必要があります。
Maven – Download Apache Maven

どのファイルを落とすかは、次の記事を参考にするとよいです。
qiita.com

自分はこんな感じのパスに配置しました
C:\Program Files\Maven\apache-maven-3.5.2

bin フォルダに環境変数のPathを通してmvn コマンドが有効になるように PC を再起動します。

次の mvn コマンドを打ってバージョン情報が表示されれば準備 OK です。
mvn -v

Apache Maven 3.5.2 (138edd61fd100ec658bfa2d307c43b76940a5d7d; 2017-10-18T16:58:13+09:00)
Maven home: C:\Program Files\Maven\apache-maven-3.5.2\bin\..
Java version: 9.0.1, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk-9.0.1
Default locale: ja_JP, platform encoding: MS932
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

MavenJava プロジェクトをビルドするにはフォルダ構成に気を使う必要があります。
まずはプロジェクトフォルダを決めて、次のようにフォルダを切ります。

HelloWorld
├─src
├─src
│ └─main
│   └─java
│     ├─ Main.java
│     └─sub
│       └─HelloWorld.java
└─pom.xml

pom.xml はテキストファイルで、Maven のビルド設定が書かれたファイルです。
POM の書き方は
Maven – Introduction to the POM
を参考にします。

とりあえず以下の
Apache Maven Compiler Plugin – Usage
Usage を参考に pom.xml に次のように書き込んで

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-module</artifactId>
  <version>1</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <properties>
  </properties>
  <dependencies>
  </dependencies>
</project>

プロジェクトフォルダにカレントを持ってきてコマンドプロンプトより
mvn package
コマンドを実行します。

すると、maven が必要なライブラリをダウンロードして、しばらくすると Build Success の表示が出ます。
target フォルダが自動的に作成され、その中に .jar ファイルが見つかります。

jar ファイルはビルド成果物群の zip 圧縮形式のファイルです。

これを実行したいので、jar を実行するコマンドを叩くのですが
java -jar .\my-module-1.jar

.\my-module-1.jarにメイン・マニフェスト属性がありません

とそのままでは実行できません。

複数のクラスの中で、どれが Main Class なのか指定してあげる必要があるとのことで
それを記述したメイン・マニフェストを jar に埋め込むため pom を次のように書き換えます。
この書き換えに関する参考ドキュメントはこちら
Apache Maven Assembly Plugin – Usage

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.simplestar.app</groupId>
  <artifactId>my-module</artifactId>
  <version>1</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>Main</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id> <!-- this is used for inheritance merges -->
            <phase>package</phase> <!-- bind to the packaging phase -->
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <properties>
  </properties>
  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl -->
    <dependency>
      <groupId>org.eclipse.jetty.websocket</groupId>
      <artifactId>javax-websocket-server-impl</artifactId>
      <version>9.4.8.v20171121</version>
    </dependency>
  </dependencies>
</project>

mvn package
コマンドを実行すると、ここで追加した jar-with-dependencies.jar ファイルも target フォルダに作成されるようになります。

これで先ほどマニフェストが無いと怒られたコマンドを再度叩くと
java -jar .\my-module-1-jar-with-dependencies.jar

Hello World!!

と正常に動作するところまで確認できます。

WebSocket サーバーを書く

ここまで来ると、あとは pom の記述方法と java のコードの書き方に気を配るだけです。
次の内容をpom の dependencies の中に書き加えます。

<!-- https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl -->
<dependency>
    <groupId>org.eclipse.jetty.websocket</groupId>
    <artifactId>javax-websocket-server-impl</artifactId>
    <version>9.4.8.v20171121</version>
</dependency>

こういう mavendependency の表記やバージョン番号は次の Maven Repository サイトの Maven タブから取得してきます。
Maven Repository: org.eclipse.jetty.websocket » javax-websocket-server-impl » 9.4.8.v20171121

WebSocket の使い方は
Using WebSocket Annotations
を参考に次のように書きました。

Main.java

package jp.simplestar.app;

import jp.simplestar.app.sub.HelloWorld;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

public class Main {
    public static void main (String[] args) {
        Server server = new Server(8000);
        WebSocketHandler wsHandler = new WebSocketHandler()
        {
            @Override
            public void configure(WebSocketServletFactory factory)
            {
                factory.register(HelloWorld.class);
            }
        };
        server.setHandler(wsHandler);
        try {
            server.start();
            server.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

HelloWorld.java

package jp.simplestar.app.sub;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;

@WebSocket
public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }

    @OnWebSocketConnect
    public void onWebSocketConnect(Session session)
    {
    }

    @OnWebSocketClose
    public void onWebSocketClose(Session session, int closeCode, String closeReason) {
    }

    @OnWebSocketMessage
    public void onWebSocketMessage(Session session, String message)
    {
        if (session.isOpen())
        {
            System.out.printf("Echoing back message [%s]%n",message);
            // echo the message back
            session.getRemote().sendString(message,null);
        }
    }

    @OnWebSocketError
    public void onWebSocketError(Session session, Throwable cause) {
    }
}

これを mvn package でパッケージ化した jar ファイルを実行します。(MainClass パスを変えたのでpomのメインクラスの指定を変える点に注意)
実行したときのコンソールの様子がこちら

java -jar .\my-module-1-jar-with-dependencies.jar
2017-12-10 15:40:46.337:INFO::main: Logging initialized @2558ms to org.eclipse.jetty.util.log.StdErrLog
2017-12-10 15:40:46.405:INFO:oejs.Server:main: jetty-9.4.z-SNAPSHOT, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8
2017-12-10 15:40:47.122:INFO:oejs.AbstractConnector:main: Started ServerConnector@7c417213{HTTP/1.1,[http/1.1]}{0.0.0.0:8000}
2017-12-10 15:40:47.122:INFO:oejs.Server:main: Started @3350ms

サーバーは機能しているみたいです。
では、確認用クライアントの方を書いていきましょう。

C# WebSocket クラアイント

次の stackOverflow の記事を参考に作ってみました。
stackoverflow.com

典型的な C# のコンソールアプリケーションです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WebSocketClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Task clientTask1 = Client();
            clientTask1.Wait();
        }

        static async Task Client()
        {
            ClientWebSocket ws = new ClientWebSocket();
            var uri = new Uri("ws://localhost:8000/");

            await ws.ConnectAsync(uri, CancellationToken.None);
            var buffer = new byte[1024];
            while (true)
            {
                Console.Write("Input message ('exit' to exit): ");
                string msg = Console.ReadLine();

                ArraySegment<byte> bytesToSend = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg));
                await ws.SendAsync(bytesToSend, WebSocketMessageType.Text, true, CancellationToken.None);

                var segment = new ArraySegment<byte>(buffer);

                var result = await ws.ReceiveAsync(segment, CancellationToken.None);

                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "I don't do binary", CancellationToken.None);
                    return;
                }

                int count = result.Count;
                while (!result.EndOfMessage)
                {
                    if (count >= buffer.Length)
                    {
                        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "That's too long", CancellationToken.None);
                        return;
                    }

                    segment = new ArraySegment<byte>(buffer, count, buffer.Length - count);
                    result = await ws.ReceiveAsync(segment, CancellationToken.None);
                    count += result.Count;
                }

                var message = Encoding.UTF8.GetString(buffer, 0, count);
                Console.WriteLine(">" + message);
            }

        }
    }
}

クライアントアプリを実行して何かを打つと…すぐに同じ内容が受信された旨が示されます。

f:id:simplestar_tech:20171210174146j:plain


サーバー側ではこのように、クライアントから送られてきたメッセージが何だったのか示されています。

f:id:simplestar_tech:20171210174215j:plain

正しくサーバー、クライアントが機能していることを確認できました。

まとめ

Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示しました。
本来なら、ここからさらに通信量を抑える努力をしていくところですかね。

今なら dependency の設定と java の一部コードを教えてもらえれば、実行可能 jar が作れる気がします。

Unity:ビヘイビアツリーなどのAIアセットと、自作

今のところ、ゲームAIを実装するなら、複雑になってもロジックの可視性が高いビヘイビアツリー(Behaviour Tree)やステートマシン(FSM)を使って実装を管理していくのか良いとされています。
なぜなら、AIの振る舞いをデザインする人間はコードを書きたくない(デザインだけに集中したい)場合が多いからです。

さっそく Behavior Designer を購入してチュートリアルビデオを半日ほど眺めてみました。
Behavior Designer と親和性の高い FSM として、Playmaker アセットがあることを知りました。
また、ビジュアルスクリプティングできる uScript Professional とも相性が良いことがわかりました。

ここで気付いたことに、これらのアセットで作成されたスクリプトは Unity に依存したコードとなるということでした。
もし完全に Unity 非依存でビヘイビアツリーの挙動がスクリプトで実現できたなら素晴らしかったのですが…残念ながらそうはいきません。

今回デザインするゲームはビューとは完全に独立したモデル領域でAIが動かなくてはならないため、Unity 非依存のビヘイビアツリーを実装することにします。

ビヘイビアツリーとは

ビヘイビアツリーはもともとはコードに書かれた内容を視覚化した階層グラフです。
Behavior Desinger のチュートリアル人工知能 Vol.32 No.2 (2017年03月号)を参考に、実装について要点をまとめてみました。

ビヘイビアツリーを構成しているノードは三つに分けられる、それぞれ Task, Composite, Decorator である。

・Task はツリーのリーフ要素で、1フレームに許された計算時間で条件判定やアクションを実行し、戻り値として1)Running(まだタスクが残っている), 2)Failure(条件に合わない、または失敗), 3)Success(条件一致、または成功) の三つのいずれかを返す。
・Composite は子ノードの実行を制御するノードで、シーケンスとセレクターの二種類が存在する。
シーケンスは最初の子ノードが実行完了するまで待機し、成功を返した場合に、次の子ノードを実行し、この操作を繰し、最後の子ノードが成功を返したら、シーケンスノードも成功を返す。一度でも子ノードが失敗を返したら、残りの子ノードは無視して失敗を返す。子ノードが実行中を返すようなら、シーケンスも実行中を返す。
セレクターは優先度順に子ノードの選択を行い、選択したノードを実行する。シーケンスと違う点は、どれか一つの子ノードが成功を返した時点で、まだ実行していない子ノードがあっても、成功を返す点である。
セレクターには指定した割合の確率で選択する Probability Selector や、1フレームにすべての子ノードを実行する Parallel がある。
・Decorator ノードは特質系で、子ノードを一つしかとることができない。ノードのタイプによって振る舞いはさまざまで、たとえば n 回子ノードを繰り返し実行してから成功を返したり、子ノードがn秒以上実行中だったら成功失敗に関わらず強制的に失敗を返したりする。

なるほどと感じたのは、ルートノードにて実行処理を行ったときに、すぐに成功失敗が返るケースは稀で、ほとんどは何かしらのタスクの実行中が返ってくることでした。
じゃ、ちょっくら実装してユニティちゃんが自律的に行動するシーンを確認してみたいと思います。

AIに身体性を与えるためのマイクロワールドの構築10:ブロック単位での移動

色々考えたのですが、エージェントはブロックの中心を結ぶ線分の上しか移動できないようにします。

f:id:simplestar_tech:20171126211354g:plain

移動が自由過ぎると、今後のAIの振る舞いや、経路計算が複雑になってしまうので、思い切ってこんなことしてみました。

制約はあれど、自然な操作で移動できるように、もう少し頑張ってみます。
しかし、Unity の実数座標から正しくモデル側のチャンクとブロックインデックスを求める処理を書くのが大変でした。(頭使った…)
でもここまで動くのを確認できれば、だいぶ考えが楽になりますので、平日の寝る前にでも手を入れて、移動部分は完成させてしまおうと思います。
これで重たくて不確定要素の多い、バグの温床となる物理計算ともおさらばです!

Unity:無料アニメーションアセットの確認

Maya のチュートリアルをずっと見てたんですけど
こりゃモーション作成は一苦労ですね。

ひとまず一般的なアクションについては外部のアセットを頼ろうと思いました。
そこで、Asset Store で無料で手に入る人気アニメーションをチェックして( ..)φメモ

Toon Soldiers WW2 demo
https://www.assetstore.unity3d.com/jp/#!/content/85702

残念ながら、腕のクロスがユニティちゃんと合わなかった。
だが、銃を撃つというモーションは珍しい

ファイティングユニティちゃん 無料お試しアセット
https://www.assetstore.unity3d.com/jp/#!/content/33478

こっちは、SDのユニティちゃんに適切に割り当たらなかった。

RPG Character Mecanim
https://www.assetstore.unity3d.com/jp/#!/content/65284

ちゃんと動く、結構あっている。
腕の振りが似合わないのと、前進がちょっと不格好だけど、見た目の破綻はなかった。
ルートモーションありで移動する良アニメ多し

Crafting Mecanim Animation Pack FREE
https://www.assetstore.unity3d.com/jp/#!/content/45047

ちゃんと動くけど、モーション数少ない
物を拾う、受け渡す系に使える

Character Pack: Free Sample
https://www.assetstore.unity3d.com/jp/#!/content/79870

ちゃんと動く、良い。
手を振る挨拶、ルートモーションのないバックモーションがある