【北海道大戦】じゃんけんバトルを実装

実際にじゃんけんバトルを実装していきます。

        protected override void OnUpdated()
        {
            asd.Vector2DF pos = asd.Engine.Mouse.Position;

            switch (_status)
            {
                case GameStatus.SelectDeffenceAction:
                    cycleProcessSelectDeffenceAction(pos);
                    break;
                case GameStatus.SelectAttackAction:
                    cycleProcessSelectAttackAction(pos);
                    break;
                case GameStatus.ShowActionResult:
                    cycleProcessShowActionResult(pos);
                    break;
            }
            if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
            {
                switch (_status)
                {
                    case GameStatus.SelectDeffenceAction:
                        onClickMouseSelectDeffenceAction(pos);
                        break;
                    case GameStatus.SelectAttackAction:
                        onClickMouseSelectAttackAction(pos);
                        break;
                    case GameStatus.ShowActionResult:
                        onClickMouseShowActionResult(pos);
                        break;
                }
            }
        }

        private void cycleProcessSelectDeffenceAction(asd.Vector2DF pos)
        {
            if(_player == Player.Deffence)
            {
                _image_gu_deffence.Show();
                _image_choki_deffence.Show();
                _image_par_deffence.Show();
                _image_gu_deffence.OnMouse(pos);
                _image_choki_deffence.OnMouse(pos);
                _image_par_deffence.OnMouse(pos);
            }
            else
            {
                var r = Singleton.Random;
                switch(r.Next(0, 3))
                {
                    case 0:
                        selectedDeffece = BattleIcon.Icon.Gu;
                        break;
                    case 1:
                        selectedDeffece = BattleIcon.Icon.Choki;
                        break;
                    case 2:
                        selectedDeffece = BattleIcon.Icon.Par;
                        break;
                }
                _status = GameStatus.SelectAttackAction;
            }
        }

        private void cycleProcessSelectAttackAction(asd.Vector2DF pos)
        {
            if (_player == Player.Attack)
            {
                _image_gu_attack.Show();
                _image_choki_attack.Show();
                _image_par_attack.Show();
                _image_gu_attack.OnMouse(pos);
                _image_choki_attack.OnMouse(pos);
                _image_par_attack.OnMouse(pos);
            }
            else
            {
                var r = Singleton.Random;
                switch (r.Next(0, 3))
                {
                    case 0:
                        selectedAttack = BattleIcon.Icon.Gu;
                        break;
                    case 1:
                        selectedAttack = BattleIcon.Icon.Choki;
                        break;
                    case 2:
                        selectedAttack = BattleIcon.Icon.Par;
                        break;
                }
                _status = GameStatus.ShowActionResult;
            }
        }

        private void cycleProcessShowActionResult(asd.Vector2DF pos)
        {
            _attackResult.SetIcon(selectedAttack);
            _attackResult.Show();
            _deffenceResult.SetIcon(selectedDeffece);
            _deffenceResult.Show();
        }

        private void onClickMouseSelectDeffenceAction(asd.Vector2DF pos)
        {
            if (_player == Player.Deffence)
            {
                if (_image_gu_deffence.IsOnMouse(pos))
                {
                    selectedDeffece = BattleIcon.Icon.Gu;
                }
                else if (_image_choki_deffence.IsOnMouse(pos))
                {
                    selectedDeffece = BattleIcon.Icon.Choki;
                }
                else if (_image_par_deffence.IsOnMouse(pos))
                {
                    selectedDeffece = BattleIcon.Icon.Par;
                }
                _image_gu_deffence.Hide();
                _image_choki_deffence.Hide();
                _image_par_deffence.Hide();
                _status = GameStatus.SelectAttackAction;
            }
        }

        private void onClickMouseSelectAttackAction(asd.Vector2DF pos)
        {
            if (_player == Player.Attack)
            {
                if (_image_gu_attack.IsOnMouse(pos))
                {
                    selectedAttack = BattleIcon.Icon.Gu;
                }
                else if (_image_choki_attack.IsOnMouse(pos))
                {
                    selectedAttack = BattleIcon.Icon.Choki;
                }
                else if (_image_par_attack.IsOnMouse(pos))
                {
                    selectedAttack = BattleIcon.Icon.Par;
                }
                _image_gu_attack.Hide();
                _image_choki_attack.Hide();
                _image_par_attack.Hide();
                _status = GameStatus.ShowActionResult;
            }
        }

        private void onClickMouseShowActionResult(asd.Vector2DF pos)
        {
            _attackResult.Hide();
            _deffenceResult.Hide();
            _status = GameStatus.SelectDeffenceAction;
        }

シーンの状態は、

  1. 防御側の選択
  2. 攻撃側の選択
  3. 結果の表示

の順で遷移していきます。

防御側選択状態では、プレイヤーが防御側ならば、じゃんけんアイコンを選択します。プレイヤーが攻撃側ならば、ランダムで相手が何を選択したかを決定します。

攻撃側選択状態は、防御側選択の逆で、プレイヤーが攻撃側ならば、じゃんけんアイコンを選択、プレイヤーが防御側ならば、ランダムで相手が何を選択したかを決定します。

そして最後に攻撃側と防御側が選択したアイコンを表示し、じゃんけんの結果を表示します。

まぁ、ここまでは表示をどうするかの処理が中心になります。

次回はじゃんけんの結果を処理する実装を行います。

【テクテクライフ】旭山記念公園と北海道神宮

思ったより難しくありませんでした。

地下鉄円山公園駅のバスターミナルから直通のバスが走ってました。

回収完了。

なかなか景色が良いです。

ちなみに、北海道神宮(円山公園内)もチェックポイントがあったので、ついでに回収しました。

リスがおりました。

【ラズパイ】【カメラ】プレビュー画面をラズパイから取得・表示

前回は、Webページをローカルで作成して、そこからブラウザに表示させていたのですが、

今回はWebページをラズパイに設置して、ブラウザからラズパイにアクセスすることによってプレビュー画面を表示させたいと思います。

動作はURLのパスによって変えます。

パスが”/”ならば、Webページを、”/Streaming”ならカメラの画像を返すようにします。

まず、Pythonのコードはこうなりました。

import base64
import cv2
import json
import os
import sys
import time
import threading

from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer
from http import HTTPStatus
from urllib.parse import urlparse

PORT = 8000

cap = cv2.VideoCapture(0)

def __main__():
    thread = threading.Thread(target=httpServe)
    thread.start()
    
    try:
        while cap.isOpened():
            time.sleep(1)
    except KeyboardInterrupt:
        return

def httpServe():
    handler = StubHttpRequestHandler
    httpd = HTTPServer(('',PORT),handler)
    httpd.serve_forever()

class StubHttpRequestHandler(BaseHTTPRequestHandler):
    server_version = "HTTP Stub/0.1"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == '/Streaming':
            enc = sys.getfilesystemencoding()

            _, img = cap.read()
            resized_img = cv2.resize(img, (480, 320))
            _, encoded_img = cv2.imencode('.jpg', resized_img, [int(cv2.IMWRITE_JPEG_QUALITY), 30])
            dst_base64 = base64.b64encode(encoded_img).decode('utf-8')

            data = {
                'image': 'data:image/jpg;base64,' + dst_base64
            }

            encoded = json.dumps(data).encode()

            self.send_response(HTTPStatus.OK)
            self.send_header("Content-type", "text/html; charset=%s" % enc)
            self.send_header("Access-Control-Allow-Origin", "null")
            self.send_header("Content-Length", str(len(encoded)))
            self.end_headers()

            self.wfile.write(encoded)
        elif parsed.path == '/vue.min.js':
            self.send_response(HTTPStatus.OK)
            with open('vue.min.js',mode='br') as f:
                data = f.read()
            self.send_header("Content-type", "text/javascript")
            self.end_headers()
            self.wfile.write(bytes(data))
        elif parsed.path == '/jquery-3.5.1.slim.min.js':
            self.send_response(HTTPStatus.OK)
            with open('jquery-3.5.1.slim.min.js',mode='br') as f:
                data = f.read()
            self.send_header("Content-type", "text/javascript")
            self.end_headers()
            self.wfile.write(bytes(data))
        else:
            self.send_response(HTTPStatus.OK)
            with open('index.html',mode='br') as f:
                data = f.read()
            self.send_header("Content-type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(bytes(data))

__main__()

無駄にでかくなった気がする。

というのも、使用しているjsファイルもラズパイからGETしようとブラウザが動くので、jsファイルを送るように作成しないといけないのです。

めんどくさいから、他のライブラリを使用することも考えないといかんなぁ。

続いて、html側。

こちらはそんなに難しくはない。

URLを変えるだけなので。

<!DOCTYPE html>
<html>
<head>
  <title>My first Vue app</title>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="vue.min.js"></script>
  <script src="jquery-3.5.1.slim.min.js"></script>
</head>
<body>
  <div id="app">
      <image id="camera" src="" />
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        timer: null,
      },
      created: function() {
        self = this;
        this.timer = setInterval(function() {self.onLoad()}, 50)
      },
      methods: {
        onLoad: function() {
            axios.get('http://pi4.local:8000/Streaming').then(function(response){
                $("#camera").attr('src', response.data.image);
            }).catch(function(error){
            });
        }
      }
    })
  </script>
</body>
</html>

これで動作しました。

【テクテクライフ】札幌中心部を攻める

今日はチェックポイントが集中している札幌中心部を歩きました。

これのためだけに北大に行ってきた。

久しぶり(2回目)だけど、ここは相変わらず広いな。

思った以上に観光客多いし。

ここから南下してJR札幌駅も回収。

旧道庁。

この右手奥に本当の道庁があるので、ここも回収。

道庁の鴨とコイ。

この近くに札幌時計台があります。

さらに大通まで行って地下鉄大通駅とさっぽろテレビ塔を回収。

さらに南下して、ニューリフレSAPPOROと市電狸小路駅、すすきの駅、地下鉄豊水すすきの駅も回収しました。

とりあえず中心部の回収は終わったので、残るは、

モエレ沼、羊ヶ丘展望台、サンピアザ水族館、滝野すずらん丘陵公園、旭山記念公園の5箇所。

んー、どこから言っても良いけど、片っ端から回るのが良いのかな。

これから考えます。

【ダイエット支援】【入力履歴機能】データベース構築

データベースをサクッと作成していきます。

$ php artisan make:migration create_eating_history_item
class CreateEatingHistoryItem extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('eating_history_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->double('protein');
            $table->double('liqid');
            $table->double('carbo');
            $table->double('calorie');
            $table->timestamps();
            $table->engine = 'InnoDB';
            $table->charset = 'utf8mb4';
            $table->collation = 'utf8mb4_unicode_ci';
        });

        Schema::create('eating_history_item_user', function (Blueprint $table) {
            $table->integer('user_id')
                  ->foreign('user_id')
                  ->references('id')->on('users')
                  ->onDelete('cascade');
            $table->integer('eating_history_item_id')
                  ->foreign('eating_history_item_id')
                  ->references('id')->on('eating_history_items')
                  ->onDelete('cascade');
            $table->engine = 'InnoDB';
            $table->charset = 'utf8mb4';
            $table->collation = 'utf8mb4_unicode_ci';
        });

        Schema::create('eating_template_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->double('protein');
            $table->double('liqid');
            $table->double('carbo');
            $table->double('calorie');
            $table->timestamps();
            $table->engine = 'InnoDB';
            $table->charset = 'utf8mb4';
            $table->collation = 'utf8mb4_unicode_ci';
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('eating_template_items');
        Schema::dropIfExists('eating_history_item_user');
        Schema::dropIfExists('eating_history_items');
    }
}
$ php artisan migrate
class EatingHistoryItem extends Model
{
    protected $table = 'eating_history_items';
}
class EatingTemplateItem extends Model
{
    protected $table = 'eating_template_items';
}
class User extends Authenticatable
{

    public function EatingHistoryItems()
    {
        return $this->belongsToMany('App\Model\EatingHistoryItem');
    }
}

データの検索は、テンプレートからだけでなく、ユーザーが入力したヒストリデータからも取得できることを想定して構築してみました。

あと、開発環境がUbuntu 18.04から20.04になったのですが、

PHPのバージョンが7.2から7.4に変わりました。

たぶん、このままだとLaravelからデータベース(mysql)にアクセスできない(could not find driver)ので、php-mysqlをインストールし直す必要があるようです。

$ sudo apt-get install php-mysql

【Cocos2d-x】Windowsとスマホで画像サイズが変わる件について

こちらの中で、スマホで表示すると画像サイズが変わるような事を書きましたが。

分かった。

sprite->getContentSize()で画像のサイズを取得することができるのですが、

Windowsとスマホではこの値が変わる。

なので、

クリッピングする時はPixel値を直接入力するのでは無くて、

このsprite->getContentSize()で取得した値を元にクリップするサイズを指定しなければならない。

    auto sprite = Sprite::create("pipo-charachip001b.png");
    if (sprite == nullptr)
    {
        problemLoading("'pipo-charachip001b.png'");
    }
    else
    {
        // position the sprite on the center of the screen
        sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

        // add the sprite as a child to this layer
        this->addChild(sprite, 0);
    }
    sprite->setScale(3.0);
    auto str = String();
    str.appendWithFormat("width %f height %f", sprite->getContentSize().width, sprite->getContentSize().height);
    label->setString(str.getCString());

    double partswidth = sprite->getContentSize().width / 3.0;
    double partsheight = sprite->getContentSize().height / 4.0;
    Vector<SpriteFrame*> animFrames;
    animFrames.reserve(12);
    animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(0,0, partswidth, partsheight)));
    animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(partswidth * 1,0, partswidth, partsheight)));
    animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(partswidth * 2,0, partswidth, partsheight)));

    Animation* animation = Animation::createWithSpriteFrames(animFrames, 0.1f);
    Animate* animate = Animate::create(animation);

    sprite->runAction(RepeatForever::create(animate));

    return true;

あとはプラットフォームに合わせて拡大すれば良い。

解決。

【Cocos2d-x】フリック処理を行う

もう一つやりたいのはフリック処理。

使用するハンドラはonTouchBeganとonTouchEndedの二つ。

タッチ開始の座標(sx,sy)とタッチリリースの座標(ex,ey)から、フリックの方向を求めるのですが、

上下左右の4方向だけだったら値の大小だけで良いかもしれませんが、

今回はアナログスティックの様な360°フリックの方向を出したいので、

ここでは三角関数を使用します。

アークタンジェント(tanの逆関数)を使用すれば、y/xから求めることができます。

関数名はatan2()です。

class SampleScene : public cocos2d::Scene
{
private:
    cocos2d::Label* label;
    cocos2d::Vec2 startPos, endPos;
    auto listener1 = EventListenerTouchOneByOne::create();
    listener1->onTouchBegan = [this](Touch* touch, Event* event)->bool
    {
        startPos = touch->getLocation();
        return true;
    };
    listener1->onTouchEnded = [this](Touch* touch, Event* event)->bool
    {
        endPos = touch->getLocation();
        double angle = atan2(endPos.y - startPos.y,endPos.x - startPos.x) * 180.0 / M_PI;
        auto str = String();
        str.appendWithFormat("angle %f", angle);
        label->setString(str.getCString());
        return true;
    };
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this);

atan2が返す値の単位はラジアンなので、これに×180/PIとすることで角度に変換できます。

よし、これで必要なことは一通りできたかな。

【テクテクライフ】平和の滝

なぜここが札幌の名所なんだろう。

この場所が結構やばくて、

これを見れば分かるだろうか。

札幌の中心部からかなり離れているのである。

行き方は、地下鉄東西線発寒南駅から、平和の滝入り口行きのバスに乗る。

そこから20分ぐらい歩く。

かなり田舎である。

漬いた。

駐車場から急な階段を降りていけば滝がある。

これだけだった。

うーん、なんでこんな所が名所なんだろう?🤔🤔🤔

登山道があって、ここから手稲山に抜けることができるみたい。

もうちょっと季節が進めば紅葉がきれいなんだろうなぁ。

ちょっと時期が早すぎたか。

プロジェクトセカイを始めた。

初音ミクなどのボーカロイド曲を題材としたリズムゲームで、

こういうゲームを見ると、どうしてもガルパと比べてしまう、バンドリーマーがおりまして。

ストーリーは主にユニット毎に用意されているのですが、

なかなか内容が重たくて。

初音ミクを初めとしたバーチャルシンガーがどのように関与してメンバーの関係をどうこうしていくのかが、見所です。

ガルパと比較してしまう、理由としては、

基本的なシステムがガルパをベースにしていること。

勿論オリジナル要素もあるのですが、

たぶん、今後リリースされるリズムゲームは、ガルパのシステムがベースになっていくんじゃ無いかと。

で、オイラのゲームの状況ですが、

初回ガチャで見事に一歌を引き当てることができ、さらに溜めた石でレンを引き当てました。

ちょっとラッキー。

まずは、ライブをこなしつつレベルを上げて、サイドストーリーとエリアアイテムを買いそろえていく、というのがメインになると思います。

そして、上級者はいきなりハードから始める方が多いと思いますが(ハードをフルコンすると、無料石がもらえる)

ただ、ガルパの経験からすると、後で必ずコインが不足します。

素材は揃っているのに。

なので、オイラはEasyからフルコンしていって、コインを貯めることを忘れずにプレイしています。

でもまぁ、これでもコインは不足する(と思う)んですけどね。

あとはイベントとかの報酬でキャラを獲得して、☆3の頭数を増やすことですかね。

それができれば序盤は戦えるはず。

ちなみに、どのくらいのレベルまで対応出来るかというと、レベル26、27でクリアできる、という腕前です。

多分難易度マスターは無理(今のところは)

そう考えると、ガルパってまだまだぬるま湯なんだなぁって思いました。

テクテクライフがリリースされました。

このゲームは、ちょっと昔にサービス終了したテクテクテクテクの後継アプリゲームです。

町中を歩いて、通ったエリアに色を塗っていく、というゲームで、

テクテクテクテクではそれにRPG的な要素も含まれていて、フィールドの敵と戦ってレベルを上げてボスを倒していく、という内容だったのですが、

今回、テクテクライフになって、そう言ったバトル要素は全て排除されました。

純粋に地図を塗っていくゲームになっています。

むしろ、こっちのほうが良かったかもしれない。

ボスが強すぎて、勝てなくて、ランクが頭打ちになってしまうという状況に陥っていたので。

その一方で、目的を持って地図を埋めていくという楽しみが増えています。

タイムスタンプラリーという機能が追加されていまして、

目的地近くに行くとチェックすることができて、コンプリートするとなにかご褒美がもらえるかもしれません。

札幌では、東豊線スタンプラリーと札幌名所スタンプラリーが用意されています。

時間があるときにいかがでしょうか。

また、今回新たに追加された便利な機能として、

バックグラウンドでもGPS信号をトレースして予約塗りすることができます。

有料ですが。

時間制と月更新のサブスク方式があるのですが、出歩くことが多い人は明らかにサブスクを利用した方がお得です。

そして、この機能を利用すると、ドラクエウォークと同時プレイができます。

最近、毎日とにかく歩きまくる、というのを実践していまして、

ドラクエウォークもやっと上級職に転職できまして、

これでさらにお出かけの楽しみが増えるというものです。

ほんと最近歩きまくってるな・・・

自分、ぼっちですが何か?