セールス採用 / グシャッてからが本当の自分だった
セールス採用 / グシャッてからが本当の自分だった
2016.07.29
第13回
電子工作部

マインクラフトが現実を動かす!『Minecraft』のスイッチで現実世界のLEDを点灯させてみた

いわたん

どうも、DevRelチャンネル外部ライターのいわたん(@iwata_n)です。Minecraftにハマっている組み込み系エンジニアです。

Minecraftをプレイしながら思いました。「ああ、ゲームの世界と現実をつなげられたらおもしろそうだな」と。みなさんもそう思いませんか?

ということで今回は、世界で大人気の箱庭ゲーム「Minecraft」と、アプリで操作できるマイコンボード「Wio Node」を使い、仮想世界と現実世界をつなげるデバイスを作ってみようと思います!

みんな大好き「Minecraft」のおさらい

minecraft

https://minecraft.net/en/

今回、現実世界とつなげる仮想世界として、みんな大好き「Minecraft」を利用します。簡単に説明すると、立方体の箱を組み合わせて自分だけの箱庭を作るというゲームです。

何でも作れる自由さと、無限にフィールドを探索でき冒険心をくすぐる仕組みで、2016年7月現在これまでに約2,400万個のゲームが売れるなど世界中でかなりの人気を博しています(参照)。

 
minecraft-1106253_1280

▲こんなのを作れるのがMinecraft

何度もクリーパーによって家を壊され、どこから打たれているのか分からないスケルトンに苦戦し、建築中に高所から落ちて死ぬMinecraft。オオカミ2匹を手懐けることに成功した時はこれで繁殖ができる!と喜ぶMinecraft。Minecraftをプレイしている人なら分かるかと思います。

そんなMinecraftですが、拡張性の高さや、ゲームの人気もあり、最近では「Project Malmo」という、Minecraft上でAIを作るプロジェクトにも利用されています。

また、MODと呼ばれる拡張機能を追加することも可能です。今回はMODを作成し、Minecraft内で起きたイベントに応じて現実世界にもイベントが起こるようにします。ちなみに自分は、Minecraftとモノが繋がる世界を勝手に「Minecraft of Things(MoT)」と呼んでいます。

こんなのが作れます

Minecraftの中でレッドストーン回路を使い、ブロックに入力すると現実世界のLEDが点灯します。1~2時間ほどで作り上げることができました。

今回使うもの

作り方

おおまかには以下の手順で作っていきます。

  1. WioNodeをセットアップ
  2. Minecraft Forgeの開発キットのセットアップ
  3. MODの開発

それでは作っていきましょう!

1. Wio Nodeのセットアップ

WioNode

まずは、ブレッドボード上にLEDを配置します。
抵抗やジャンパワイヤを写真のように刺してみてください。

 

IMG_20160721_212723

実際に配線した写真がこちらです。

次にWio Nodeの設定をします。今回は、Wio NodeのD0にLEDを配線します。こちらの記事の「1. Wio Nodeのセットアップ」を参考にしてみてください(1-4だけ変更となります)セットアップが完了したら、

 
LEDのカソード側(写真では黒いジャンパワイヤ)をGroveのケーブルの黒色へ、LEDのアノード側(写真では黄色のジャンパワイヤ)をケーブルの黄色へと接続します。

配線後に、アプリからD1にGeneral Digital Outputを設定します。

 

Screenshot_20160721-122727

上記のスクリーンショットのように設定します。

2. Minecraft Forgeの開発キットのダウンロード

Minecraft_Forge

Wio Nodeのセットアップが終わったら、Minecraft Forgeの開発キット(MDK)を公式サイトよりダウンロードします。

今回は、2016年7月現在で最新の1.10.2を使用します。

注意
もし、バージョンが違っている場合には「Minecraft Versions」から選択することが可能です。Minecraft Forgeはバージョンごとで使えるAPIが変わることが多いため、別バージョンだと本手順が異なる可能性があります。

 
Screen Shot 2016-07-14 at 20.31.37

ダウンロードしたMDKを解凍します。

IntelliJで開く

ダウンロードしたプログラムは、JetBrainsが開発しているIDE「IntelliJ」で開きます。

まずターミナルなどでファイルがある場所まで移動し、以下のコマンドを実行します。OSに合ったコマンドを実行してください。Windowsの場合は以下です。

gradlew setupDecompWorkspace

Linux/Mac OSの場合は以下です。

./gradlew setupDecompWorkspace

 
Screen Shot 2016-07-14 at 23.53.59

コマンドが成功したら、IntelliJでプロジェクトをインポートします。

 
Screen Shot 2016-07-15 at 00.10.58

この際に、build.gradleを選択するように注意してください。

 
Screen Shot 2016-07-15 at 00.11.10

この画面では、そのままOKボタンをクリックします。

注意
使用している環境によってはJVMが入っていない可能性がありますので、その場合はJVMのインストールをおこなう必要があります。ダウンロードは下記リンクからおこなえます。
http://www.java.com/ja/download/

インポートが完了したら、ターミナル等で以下のコマンドを実行してください。Windowsの場合は以下です。

gradlew genIntellijRuns

Linux/Mac OSの場合は以下です。

./gradlew genIntellijRuns"

以下のような出力があれば成功です。

~略~
#################################################
         ForgeGradle 2.2-SNAPSHOT-0447b4e
  https://github.com/MinecraftForge/ForgeGradle
#################################################
               Powered by MCP unknown
             http://modcoderpack.com
         by: Searge, ProfMobius, Fesh0r,
         R4wk, ZeuX, IngisKahn, bspkrs
#################################################
:genIntellijRuns

BUILD SUCCESSFUL

Total time: 37.125 secs

This build could be faster, please consider using the Gradle Daemon: https://docs.gradle.org/2.7/userguide/gradle_daemon.html

 
Screen Shot 2016-07-15 at 00.46.17

IntelliJが警告を出すことがあります。これは、IntelliJ以外でプロジェクトファイルが変更された場合に出てくる警告なので問題ありません。「Yes」を選択します。

 
Screen Shot 2016-07-15 at 00.12.15

ここまでの工程が正常に終了すると、上のような構成になります。

 

Screen-Shot-2016-07-15-at-01.04.35

右上の実行構成を選ぶコンボボックスから「Minecraft Client」を選択し、実行ボタンを押します。

 
Screen Shot 2016-07-15 at 00.55.08

このとき、画像のようなエラーが出た場合、「Minecraft Client」から「Edit Configurations……」を押してClasspathの設定をおこなう必要があります。

 
Screen Shot 2016-07-15 at 00.55.31

上記のように「Use classpath of module:」で「forge-1.10.2-12.18.1.2014-mdk_main」を選択します。

実行ボタンを押すことでMODが導入されたMinecraftが起動します。これで、開発するための環境が整いました。


3. MODの開発

いよいよ終わりに近づいてきました! あとひと踏ん張り、がんばりましょう。
MODの作成としては以下の手順となります。

0. Wio Nodeと通信するクラスの実装
1. ブロックの実装
2. テクスチャなどのリソースを適応
3. Minecraftにブロックの登録

今回はテクスチャの用意などは手間がかかるため、ブロックの実装をメインに説明します。

Wio Nodeと通信するクラス

まずは、Wio Nodeと通信するためのクラスを実装します。

ここはMinecraftとは関係無い実装です。Wio Nodeとの通信はSSL通信となるため少し手間がかかります。ソースコード全文は以下です。

WioClient.javaとして src/main/java/com.example.examplemod に保存します。

package com.example.examplemod;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Wio Nodeと通信するクラス
 */
public class WioClient {
    private static Logger logger = LogManager.getLogger("WioDigitalOutputBlock");
    private static final String POST = "POST";
    private static final String GET = "GET";
    private final String token;

    private String address = "";

    /* Https通信 */
    private HttpsURLConnection httpsURLConnection;

    /* タスク */
    private ExecutorService executorService;

    /* 通信結果のコールバックのインスタンス */
    private onResultListener resultListener;

    /**
     * 通信結果のコールバックのインターフェース
     */
    public interface onResultListener {
        /**
         * 成功時のコールバック
         * @param message 応答
         */
        void onSuccess(BufferedReader message);

        /**
         * エラー時のコールバック
         * @param message エラーメッセージ
         */
        void onError(String message);
    }

    /**
     * SSL通信用
     */
    class LooseTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }

    /**
     * 別スレッドで実行しないとMinecraftの世界がフリーズしてしまう
     */
    class RequestTask implements Runnable {
        /* HTTPS通信で使用するメソッド */
        String method;

        /* HTTPS通信に付与するクエリ */
        String query;

        public RequestTask(String method, String query) {
            this.method = method;
            this.query = query;
        }

        @Override
        public void run() {
            String message = "";
            try {
                URL url = new URL(address + query + "?access_token=" + token);
                logger.debug(url.getPath());
                httpsURLConnection = (HttpsURLConnection) url.openConnection();
                httpsURLConnection.setRequestMethod(method);
                httpsURLConnection.setInstanceFollowRedirects(false);
                SSLContext sslContext = SSLContext.getInstance("SSL");
                sslContext.init(null,
                        new X509TrustManager[] {new LooseTrustManager()},
                        new SecureRandom());
                httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
                httpsURLConnection.connect();

                BufferedReader reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
                // 成功したらコールバックする
                if (resultListener != null) {
                    resultListener.onSuccess(reader);
                }
                httpsURLConnection.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
                message = e.getMessage();
            } catch (NoSuchAlgorithmException e) {
                message = e.getMessage();
                e.printStackTrace();
            } catch (KeyManagementException e) {
                message = e.getMessage();
                e.printStackTrace();
            } finally {
                // エラーをコールバックする
                if (resultListener != null) {
                    resultListener.onError(message);
                }
            }
        }
    }

    public WioClient(String address, String token) {
        this.token = token;
        this.address = address;
        this.executorService = Executors.newSingleThreadExecutor();
    }

    /**
     * リクエスト
     * @param method 通信に使用するメソッド
     * @param query クエリ
     */
    protected void request(String method, String query) {
        executorService.execute(new RequestTask(method, query));
    }

    /**
     * POST
     * @param query
     */
    public void post(String query) {
        request(POST, query);
    }

    /**
     * GET
     * @param query
     */
    public void get(String query) {
        request(GET, query);
    }

    /**
     * コールバックの登録
     * @param resultListener
     */
    public void setOnResultListener(onResultListener resultListener) {
        this.resultListener = resultListener;
    }
}

このクラスでは、Wio Nodeは接続されたGroveのモジュールごとにREST APIを利用するため、POSTとGETメソッドを実装しています。例えば、D1ポートに接続された汎用デジタル出力をHigh状態にする場合は、以下のようなコードになります。

WioClient wioClient = new WioClient("https://iot.seeed.cc/v1/node/GenericDOutD1/onoff/", "トークンをここに設定する");
wioClient.post("1");

ブロックを実装する

いよいよMinecraftのブロックを実装します。まずは、実装全文です。

package com.example.examplemod;

import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.annotation.Nullable;
import java.util.Random;

/**
 * レッドストーンの入力に応じてWio Nodeの汎用デジタル出力の状態を変更するブロック
 */
public class WioDigitalOutputBlock extends Block {
    private static Logger logger = LogManager.getLogger("WioDigitalOutputBlock");

    private WioClient wioClient = new WioClient("https://iot.seeed.cc/v1/node/GenericDOutD1/onoff/", "トークンをここに設定する");

    /* RedStoneの入力状態 */
    private boolean isOn = false;

    public WioDigitalOutputBlock() {
        super(Material.IRON);
        setCreativeTab(CreativeTabs.REDSTONE);
        setUnlocalizedName("WioNode");
    }

    /**
     * ブロックの周辺環境が変化した際に呼び出される
     * @param state
     * @param worldIn
     * @param pos
     * @param blockIn
     */
    public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn) {
        BlockPos blockpos1 = pos.up();
        boolean flag = worldIn.isBlockPowered(pos) || worldIn.isBlockPowered(blockpos1);

        /* クライアントの場合のみ実行 */
        if (!worldIn.isRemote) {
            if (flag != isOn) {
                logger.info("neighborChanged post On");
                worldIn.scheduleUpdate(pos, this, 4);
            }
        }
        isOn = flag;
    }

    /**
     * Tickの更新時に呼び出される
     * @param worldIn
     * @param pos
     * @param state
     * @param rand
     */
    public void updateTick(World worldIn, BlockPos pos, IBlockState state, Random rand) {
        if (isOn) {
            wioClient.post("1");
            logger.info("post On");
        } else {
            wioClient.post("0");
            logger.info("post Off");
        }
    }

    /**
     * Get the Item that this Block should drop when harvested.
     */
    @Nullable
    public Item getItemDropped(IBlockState state, Random rand, int fortune) {
        return Item.getItemFromBlock((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
    }

    public ItemStack getItem(World worldIn, BlockPos pos, IBlockState state) {
        return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
    }

    protected ItemStack createStackedBlock(IBlockState state) {
        return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
    }
}

それぞれのメソッドについて解説します。

WioDigitalOutputBlock

public WioDigitalOutputBlock() {
    super(Material.IRON);

    // Creativeモードで表示されるタブの設定
    setCreativeTab(CreativeTabs.REDSTONE);

    setUnlocalizedName("WioNode DigitalOutput");
}

コンストラクタです。ここでは材質の設定やCreativeモードで、アイテムがどこに表示されるかなどを設定します。

neighborChanged

public void neighborChanged(IBlockState state, World worldIn, BlockPos pos, Block blockIn) {
    BlockPos blockpos1 = pos.up();

    // 自分自身か自分の1個上のブロックにレッドストーンの入力があるか判定する
    boolean flag = worldIn.isBlockPowered(pos) || worldIn.isBlockPowered(blockpos1);

    /* クライアントの場合のみ実行 */
    if (!worldIn.isRemote) {
        if (flag != isOn) {
            logger.info("neighborChanged post On");
            // 状態の更新を予約
            worldIn.scheduleUpdate(pos, this, 4);
        }
    }
    isOn = flag;
}

neighborChangedは、周辺のブロックの状態が変化した際に呼び出されるメソッドです。このメソッドでは、レッドストーンの入力があったかを判定し、ブロックの状態の更新を設定します。

updateTick

public void updateTick(World worldIn, BlockPos pos, IBlockState state, Random rand) {
    if (isOn) {
        // ONであればLEDをつける
        wioClient.post("1");
        logger.info("post On");
    } else {
        // OFFであればLEDを消す
        wioClient.post("0");
        logger.info("post Off");
    }
}

MinecraftはTickという周期的な処理の中で動作しています。そのTickが発生した際に呼び出されるメソッドです。ここでは neighborChanged で判定したレッドストーンの入力に応じて、Wio Nodeへのリクエストをおこなっています。

その他

@Nullable
public Item getItemDropped(IBlockState state, Random rand, int fortune) {
    return Item.getItemFromBlock((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
}

public ItemStack getItem(World worldIn, BlockPos pos, IBlockState state) {
    return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
}

protected ItemStack createStackedBlock(IBlockState state) {
    return new ItemStack((Block)Block.REGISTRY.getObject(new ResourceLocation("wioNode DigitalOutput")));
}

今回のWio Nodeとの連携では不要ですが、ブロックの実装上必要な処理です。ブロックが破壊されたときなどにドロップするブロックの設定などをおこないます。

ブロックを登録する

実装したブロックをMinecraftに登録します。ExampleMod.javaはMinecraft ForgeのMDKをダウンロードするとはじめから同梱されているので、実装を追記します。

package com.example.examplemod;

import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemBlock;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION)
public class ExampleMod
{
    public static final String MODID = "MoT";
    public static final String VERSION = "1.0";

    public static Block wioDigitalOutputBlock;

    @EventHandler
    public void preInit(FMLInitializationEvent event) {
        wioDigitalOutputBlock = new WioDigitalOutputBlock();

        ResourceLocation wioBlockDoRegistryName = new ResourceLocation(MODID, "wioNode DigitalOutput");
        ItemBlock wioDoItemBlock = new ItemBlock(wioDigitalOutputBlock);
        GameRegistry.register(wioDigitalOutputBlock, wioBlockDoRegistryName);
        GameRegistry.register(wioDoItemBlock, wioBlockDoRegistryName);
    }

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        // some example code
        System.out.println("DIRT BLOCK >> "+Blocks.DIRT.getUnlocalizedName());
    }
}

 

Screen-Shot-2016-07-15-at-01.04.35

ここまでの実装ができたら、右上の実行構成を選ぶコンボボックスから「Minecraft Client」を選択し、実行ボタンを押します。

 
loading

すると、Minecraftが起動します。普通のMinecraftと違い、はじめにいろいろとロードしてるのが分かるかと思います。この段階で、作成したMODを読み込んでいるんです。

 
minecraft

起動しました! もしここで起動しなかった場合、作成したMODに何かしらのバグがある可能性がありますので、IntelliJのコンソールログにエラーが出ていないかを確認しましょう。

 
load_error

起動したら、(いつも通りに)ワールドを作成してみましょう。もし、作成後に画像のような画面が出た場合、ワールドで使用していたMODのブロックが見つからないのが原因となっています。これは、ブロックの登録を削除した場合などに見られます。

これが出た場合は「Yes」を押しましょう。

 
list

ワールドが作成できたら、「E」を押してインベントリを開き、レッドストーンのタブを開きます。すると、今回作成したブロックが登録されているのが分かると思います。

今回はテクスチャの登録をおこなっていないので、デフォルトの紫と黒のチェック柄が表示されます。

 
set

登録したブロックに、レバーからのレッドストーンの入力が入るように配置します。

完成!

on

配置ができたらレバーをON/OFFしてみましょう。

 
DSC_0618

レッドストーンのON/OFFに応じてWio Nodeに接続したLEDが点滅するかと思います。少し見にくいですが、ブレッドボード上のLEDが点灯しています!

Minecraftと現実世界をつなげることができました! おつかれさまでした!

注意点

今回のMODを作るにあたり、いくつか注意すべき点があります。

遅延が発生する

MinecraftからWio Nodeへインターネットを経由して接続をおこなっているため、どうしても遅延が発生します。

Minecraft上でクロック回路を作成し、Wio Nodeへイベントを定期的におこないたい場合など、先のリクエストが処理されていない状態で、次のリクエストが来たときなどに遅れて処理されてしまいます。

ライセンスに反しないようにする

自作したデバイスを販売したい場合、Minecraftのライセンスへの注意が必要になります。Minecraft Commercial Usage Guidelinesには、『同じ (または相当部分が似ている) 独自デザインを使用して 20 個を超える製品アイテムを作成および販売しない』などの条件がありますので、販売をおこなう場合は一度ライセンスを確認しておきましょう。

・Minecraft Commercial Usage Guidelines
https://account.mojang.com/documents/commercial_guidelines

MODの開発が難しい

Minecraft Forgeは比較的更新頻度が高いです。ドラスティックな変更も多く、過去の情報が使えないということも多々起こります。IntelliJでデコンパイルしたコードから、他のブロックのコードを参考にするなどなかなか苦労することが多いです。

おわりに

ということで今回はマイコンボード「Wio Node」を使い、仮想現実「Minecraft」を現実世界とつなげてみました。(Wio Nodeをゲットしたい方はこちら

仮想現実というと「VRゴーグル」を思い浮かべる方も多いかもしれませんが、自分は乱視なため、よく見えないことが多いです。個人的には、もっと仮想現実と現実を物理的に繋げる、拡張現実に近い意味での仮想現実を実現していきたいなと思っています。

今後は、現実から仮想現実へのイベントの通知などを実装していこうと思います!