LoginSignup
8
7

More than 5 years have passed since last update.

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

Posted at

はじめに

前々回前回と真面目にMeteorの説明をしてきたのですが、今回は遊んでみました。

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

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

簡単ですが、描いてみました。
meteon_3.png

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

ゲーム仕様

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

Monster Client

<基本機能>

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

<演出面>

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

Sword Client

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

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

基本構成

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

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

めておん概要.png

パッケージの説明

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

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を2種類作成しました。

game_main

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

p_attack

攻撃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

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

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

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

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7