はじめに
前々回、前回と真面目にMeteorの説明をしてきたのですが、今回は遊んでみました。
モンスターと戦うという単純なゲームです。
Clientは2種類あり、モンスターが表示されるClient(Monster Clientとします)と攻撃するためのAndroid端末用のClient(Sword Clientとします)です。
端末で攻撃するとモンスターがダメージを受けるという内容です。(単純ですね)
しかし、となるとまずはプログラムの前にモンスターが必要ですね。
どっか既視感があるデザインですが、気にしてはいけません。
名前は「めておん」と名付けてみました。(安直な・・・)
さて、あとはめておんをひたすら殴るためのコードを書くだけです。
ゲーム仕様
その前にもう少し詳細なゲームの仕様を整理します。
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はそれらのデータの変化を検出して
リアクションを発動させるようにしました。
パッケージの説明
では内容の説明には入ります。
まずはパッケージの説明です。
今回は以下の2つのパッケージを追加しています。
gentlenode:jrumble
jquery
今回は一部Jqueryを使っています。また、めておんがダメージを受けた時のぶるぶる表現にjrumbleを使いました。
HTMLファイルの解説
次に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のコードです。
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アプリの解説
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)
今回は遊びなので実装優先ということでご容赦ください。
完成
ということで完成しました。
今回はMeteorを使って遊んでみよー!という感じで作ってみましたが、
Meteorの機能をちゃんと使い切れているのか自信がありません;;
ま、まあ楽しければいいのだ!!
一応実際の動画をYouTubeにアップしたのでご興味がある方はご覧ください。
http://youtu.be/vaX3GyC32Zs
ソースコードは後日アップするかもしれません。