MeteorとAndroidでゆうしゃのつるぎを作ってみた


#はじめに
[前々回](http://qiita.com/Hironori777/items/041e37c5dd696191dd81)、[前回](http://qiita.com/Hironori777/items/dd0710d925011ab54a28)と真面目にMeteorの説明をしてきたのですが、今回は遊んでみました。

モンスターと戦うという単純なゲームです。
Clientは２種類あり、モンスターが表示されるClient（Monster Clientとします）と攻撃するためのAndroid端末用のClient（Sword Clientとします）です。
端末で攻撃するとモンスターがダメージを受けるという内容です。（単純ですね）

しかし、となるとまずはプログラムの前にモンスターが必要ですね。

簡単ですが、描いてみました。
![meteon_3.png](https://qiita-image-store.s3.amazonaws.com/0/64855/e7667236-cbe3-9abb-ddcf-e86a39bd84f9.png)

どっか既視感があるデザインですが、気にしてはいけません。
名前は「<B>めておん</B>」と名付けてみました。（安直な・・・）
さて、あとはめておんをひたすら殴るためのコードを書くだけです。

#ゲーム仕様

その前にもう少し詳細なゲームの仕様を整理します。

##Monster Client
＜基本機能＞

+ モンスターとプレイヤーは初期状態でHP100をもっている
+ プレイヤーが攻撃するとモンスターのHPが10減る
+ モンスターが攻撃するとプレイヤーのHPが10減る
+ モンスターは５秒周期で攻撃する
+ モンスターのHPが０になったらプレイヤーの勝ち
+ プレイヤーのHPが０になったらプレイヤーの負け

＜演出面＞

+ 戦闘中はBGMが流れている
+ プレイヤーの攻撃が発生すると攻撃音が鳴る
+ モンスターの攻撃が発生するとダメージ音が鳴る
+ 戦いに勝つと勝利のBGMが鳴る
+ 戦いに負けると敗北のBGMが鳴る
+ プレイヤーの攻撃が発生するとモンスターの画像とHP表示がぶるぶるする
+ モンスターの攻撃が発生するとプレイヤーのHP表示がぶるぶるする

##Sword Client

+ Android端末を振ると、モンスターに攻撃できる
+ モンスターからダメージを受けた時はバイブ動作をする

まあ、ゲームとしてはよくあるスタイルですね。

# 基本構成

このゲームは攻撃アクションとダメージ効果が双方向で連動する必要があります。
いろいろと実現方法はありそうですが、せっかく勉強したMeteorというフレームワークを最大限利用してみることにしました。

今回はモンスターのHPとプレイヤーのHPをそれぞれMongoDBに登録し、双方のClientはそれらのデータの変化を検出して
リアクションを発動させるようにしました。

![めておん概要.png](https://qiita-image-store.s3.amazonaws.com/0/64855/20647cc6-f17a-8b0a-e9b7-039595c8794c.png)


# パッケージの説明

では内容の説明には入ります。
まずはパッケージの説明です。
今回は以下の２つのパッケージを追加しています。

>gentlenode:jrumble
>jquery

今回は一部Jqueryを使っています。また、めておんがダメージを受けた時のぶるぶる表現にjrumbleを使いました。

#HTMLファイルの解説

次にHTMLの公開です。
今回はどーんといきます。

```mysword.html
<head>
  <title>mysword</title>
<script type="text/javascript">
	function sendAction(){
		document.getElementById("p_attack").click();
	}
</script>
<style type="text/css">

body {
background-color: #000000;
color: #ffffff;
}
</style>
</head>

<body>
  <center>
  <h2>ゆうしゃのつるぎでたたかえ！</h2>
  {{> game_main}}
  </center>
</body>


<template name="game_main">
  {{#if mode}}
     <p>SWORD</p>
     {{player_damage}}
     {{> p_attack}}
  {{else}}
     <div id="mon_area">
     <img id="mon_img" src="meteon_3.png">
     <h3>MONSTER:<BR>{{monster_hp.value}}</h3>
     </div>
      <div id="pl_area">
     <h3>PLAYER:<BR>{{player_hp.value}}</h3>
      </div>
　  <h3 id="battle_msg"></h3>
  {{/if}}
</template>

<template name="p_attack">
  <input style="visibility:hidden" type="button" id="p_attack" class="attack_mon" value="attack">
</template>
```

今回はTemplateを２種類作成しました。

<B>game_main</B>

Client本体です。
リクエストパラメータを判断してモンスター側と攻撃側の２種類のClientに切り替わります。

<B>p_attack</B>

攻撃UI用のTemplateです。
ボタンを配置していますが、ここは端末が振られた時にonClickイベントを投げるためだけに用意しましたので表示しないようにstyle="visibility:hidden"としています。
onClickイベントは先頭のJavascriptコード内のsendAction関数から発行しています。
この関数は端末が振られた際にAndroid側から呼び出されます。

#JavaScriptファイルの解説

さて次はJSのコードです。

```mysword.js
GmParameters = new Mongo.Collection('gm_params');
if (Meteor.isClient) {
   var sw_mode=false;
   var attack_timer;

   //Sound variable
   var bgm_sound;
   var mon_sound;
   var pl_sound;
   var win_sound;
   var lose_sound;
  
   //Audio初期化
   function initSound(src){
       var sound = new Audio("");
       sound.autoplay = false;
       sound.src = src;
       sound.load();
       return sound;
   }

   //リクエストパラメータ判定
   //mode=swだったらSWORDモード、それ以外はMonsterモード
   function getMode() {
       var url   = location.href;
       console.log("url="+url);
       
       var rtn='none';
       parameters    = url.split("?");
       if(parameters.length>1){
         params   = parameters[1].split("&");
         var paramsArray = [];
         for ( i = 0; i < params.length; i++ ) {
             element = params[i].split("=");
             paramsArray.push(element[0]);
             paramsArray[element[0]] = element[1];
         }
         rtn = paramsArray["mode"];
       }
       return rtn;
   }

   // ダメージ発生時のぶるぶる表示
   function rumble(id){
         $(id).jrumble({
	        x: 5,
	        y: 0,
	        rotation: 5
         });
         $(id).trigger('startRumble');
         Meteor.setTimeout(function(){
              $(id).trigger('stopRumble');
         },200);
   }
  
   //戦闘メッセージの切り替え
   function update_msg(msg){
         var elem = document.getElementById("battle_msg");
         elem.innerHTML=msg
   }
  
   //モンスターが攻撃！！
   function monster_attack(){
          Meteor.call('update_data','pl_hp',-10);
   }

　 //初期処理
   Template.game_main.created = function() {
      //init
       bgm_sound = initSound("game_maoudamashii_1_battle37.ogg");
       bgm_sound.play();
       
       mon_sound = initSound("punch-high2.mp3");
       pl_sound  = initSound("sword-slash4.mp3");
       win_sound = initSound("game_maoudamashii_9_jingle01.mp3");
       lose_sound= initSound("game_maoudamashii_9_jingle07.mp3");

       if(getMode()=="sw"){
         sw_mode=true;
       }

       Meteor.call('reset_data','mon_hp',100);
       Meteor.call('reset_data','pl_hp',100);
   }

   Template.game_main.helpers({
    //Clientモードの取得
    mode: function(){
      console.log("mode="+sw_mode);
      return sw_mode;
    },
    //MonsterのHPを取得
    monster_hp: function(){
         mon_hp = GmParameters.findOne({key:'mon_hp'});
         if(mon_hp != undefined){
            console.log("Monster HP:"+mon_hp.value);
             
            if(mon_hp.value!=100){
       		pl_sound.play();
                rumble('#mon_area');
            }
            if(mon_hp.value<=0){
                Meteor.clearInterval(attack_timer);
                bgm_sound.pause();
                win_sound.play();
                document.getElementById('mon_img').src = 'meteon_noimg.png';
                update_msg("めておん をたおした！！！");
                mon_hp = 0;
            }
         }
         return mon_hp;
    },
    //PlayerのHPを取得
    player_hp: function(){
         pl_hp = GmParameters.findOne({key:'pl_hp'});
         if(pl_hp != undefined){
            console.log("Player HP:"+pl_hp.value);
            if(pl_hp.value!=100){
       		mon_sound.play();
                rumble('#pl_area');
            }
            if(pl_hp.value<=0){
                bgm_sound.pause();
                lose_sound.play();
                update_msg("たたかいにまけてしまった・・・・");
                Meteor.clearInterval(attack_timer);
            }
         }
         return pl_hp;
    },
    player_damage: function(){
         pl_dmg = GmParameters.findOne({key:'pl_hp'});
         if(pl_dmg != undefined){
            if(pl_dmg.value!=100){
		window.jsif.Vibrator();
	    }
	 }
    }
   });
  
   Template.p_attack.events({
    'click .attack_mon':function(){
         mon_hp = GmParameters.findOne({key:'mon_hp'});
         if(mon_hp != undefined){
            if(mon_hp.value<=0){
		return;
	    }else{
         	Meteor.call('update_data','mon_hp',-10);
  	    }
	 }
    }
   });

   //DOM生成完了後の処理
   Meteor.startup(function() {
       if(sw_mode==false){
           update_msg("めておん があらわれた！");
           //5秒周期でMonsterは攻撃！
           attack_timer = Meteor.setInterval(monster_attack,5000);
        }
   });
}

if (Meteor.isServer) {
   Meteor.methods({
                'update_data':function(type,data){
                         GmParameters.update({key:type}, {$inc:{value:data}});
                },
                'reset_data':function(type,data){
                         param = GmParameters.findOne({key:type});
                         console.log("Reset_data:"+param);
                         if(param==undefined){
                               console.log("insert Data:"+type);
                               GmParameters.insert({
                                   key:type,
                                   value:data
                               });
                         }else{
                               console.log("update Data:"+type);
                              GmParameters.update({key:type}, {$set:{value:data}});
                         }
                }
   });  


   Meteor.startup(function () {
    // code to run on server at startup
   });
}
```

ポイントだけ説明します。

## 初期処理

>Template.game_main.created

createdはTemplateが呼ばれた時に最初にコールされるメソッドのようです。
ここでこのアプリケーションの初期処理として以下のことを行っています。

+ BGMや効果音用のAudio要素の生成
+ Clientのモード判定（GETパラメータがmode=swの時のみSword Clientとする）
+ Monster及びPlayerのHP初期化

ただ、この時点ではDOMは構成されていないようで、getElementByIdをコールしても
Undefinedが帰ってきていました。

DOM生成完了後の処理を描きたい場合はMeteor.startupに記述する必要があるようです。

## game_mainテンプレートのヘルパー

今回以下のヘルパーを作成しています。

+ mode
Clientのモードを返すだけのヘルパーです。今結果によって、Client側は表示内容を切り替えるようにしています。

+ monster_hp
モンスター側のHP取得用ヘルパーです。DB上のモンスターのHP値が変わる都度呼び出されますので、ダメージ効果処理と勝利判定をここでやっています。

+ player_hp
プレイヤー側のHP取得用ヘルパーです。同じく、プレイヤー側のダメージ処理、敗北判定を行っています。

+ player_damage
Sword Client側でしか使わないヘルパーです。ダメージを受けた際のAndroid側でのバイブ起動用のヘルパーです。

## p_attackテンプレートのイベント

+ click .attack_mon

プレイヤー側の攻撃イベントです。


## Server側Method

+ update_data
DB更新用メソッドです。

+ reset_data
初期化用メソッドです。データが存在しない場合はinsert,存在する場合はupdateしています。

# Androidアプリの解説

```MySwordAppActivity.java
package com.uboy.app.myswordapp;

import java.util.List;

import android.app.Activity;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.TextView;

public class MySwordAppActivity extends Activity  implements SensorEventListener{
	private SensorManager manager;
	WebView wv = null;
	TextView tv = null;
	private final String TAG = "MySword"; 
	private float mLastX = -1.0f, mLastY = -1.0f, mLastZ = -1.0f;
	private long mLastTime;
	private int sensor_sleep=0;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_my_sword_app);
		wv = (WebView)findViewById(R.id.webView1);
		wv.loadUrl("http://192.168.0.15:3000?mode=sw");

		tv = (TextView)findViewById(R.id.textView1);
		tv.setTextColor(Color.RED);
		tv.setText("");
		
		WebSettings wbset= wv.getSettings();
		wbset.setJavaScriptEnabled(true); //JavaScript有効

		//InterFaceの追加
		wv.addJavascriptInterface(new JSInterface(), "jsif"); //
		mLastTime = System.currentTimeMillis();
		
		manager = (SensorManager) getSystemService(SENSOR_SERVICE);

		List<Sensor> sensors = manager.getSensorList(Sensor.TYPE_ACCELEROMETER);
		if(sensors.size() > 0) {
			Sensor s = sensors.get(0);
			manager.registerListener(this, s, SensorManager.SENSOR_DELAY_FASTEST);
		}

	}
	@Override
	protected void onStop() {
		super.onStop();
		// Listenerの登録解除
		manager.unregisterListener(this);
	}

	@Override
	protected void onResume() {
		super.onResume();
		// Listenerの登録
		List<Sensor> sensors = manager.getSensorList(Sensor.TYPE_ACCELEROMETER);
		if(sensors.size() > 0) {
			Sensor s = sensors.get(0);
			manager.registerListener(this, s, SensorManager.SENSOR_DELAY_UI);
		}
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
	}

　  //Sensorイベントハンドラ
	@Override
	public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {

        	if(sensor_sleep > 0){
        		sensor_sleep--;
        		return;
        	}
        	
        	long now = System.currentTimeMillis();
            long diff = now - mLastTime;
            
            float speedX = Math.abs(event.values[SensorManager.DATA_X]-mLastX);
            float speedY = Math.abs(event.values[SensorManager.DATA_Y]-mLastY);
            float speedZ = Math.abs(event.values[SensorManager.DATA_Z]-mLastZ);
            float speed  = (speedX+speedY+speedZ)/diff * 1000;
            
            if((speed!=Float.POSITIVE_INFINITY)&&(speed > 4500)){  //閾値は適当・・・
            	String spd = "SPEED="+speed;
            	Log.i(TAG,spd);
            	tv.setText(spd);
            	sendAction();
            	sensor_sleep = 10;
            }
			mLastTime = now;
			mLastX = event.values[SensorManager.DATA_X];
			mLastY = event.values[SensorManager.DATA_Y];
			mLastZ = event.values[SensorManager.DATA_Z];

        }
	
	}
	
	//Event発生をWebView側に通知
	public void sendAction(){
		wv.loadUrl("javascript:sendAction()");
	}
	
	//WebViewのI/F用クラス
	final class JSInterface { 
		JSInterface() { 
		}

        //バイブ実行
		@JavascriptInterface
		public void Vibrator(){
			  Log.i(TAG, "Vibrator");
	          Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
	          vibrator.vibrate(500);
		}
	}
}
```
端末の振りの検出はSensorイベントから取得しています。
速さの閾値は結構適当です。

ちなみに、今回Javascriptとの連携にaddJavascriptInterfaceを使ったんですが、セキュリティ的には問題があります。

> Developersサイトにも書いてあります。
http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object,%20java.lang.String)

今回は遊びなので実装優先ということでご容赦ください。

# 完成

ということで完成しました。

![スクリーンショット 2015-03-03 23.38.19.png](https://qiita-image-store.s3.amazonaws.com/0/64855/9be657c3-a4f3-f8af-6508-f6c74eb190be.png)

今回はMeteorを使って遊んでみよー！という感じで作ってみましたが、
Meteorの機能をちゃんと使い切れているのか自信がありません；；
ま、まあ楽しければいいのだ！！

一応実際の動画をYouTubeにアップしたのでご興味がある方はご覧ください。
http://youtu.be/vaX3GyC32Zs

ソースコードは後日アップするかもしれません。