https://github.com/takishita2nd/HokkaidoWar
最初のプレイヤーの都市選択で確認ダイアログを出すようにしました。
あとは、最後までプレイできることも確認しました。
ただ、戦力差がありすぎると、バトルがワンパンで決着が付いてしまうのが、ちょっとよろしくないですね。
		https://github.com/takishita2nd/HokkaidoWar

最初のプレイヤーの都市選択で確認ダイアログを出すようにしました。
あとは、最後までプレイできることも確認しました。
ただ、戦力差がありすぎると、バトルがワンパンで決着が付いてしまうのが、ちょっとよろしくないですね。
		
前回のままだと、クライアント側(ブラウザ)を撮影中に閉じてしまうと、動画撮影を終了する人がいなくなってしまいます。
これを防ぐためには、クライアントが生存していることを常に確認する処理が必要になります。
まぁ、今回はプレビュー画面で常にデータのやりとりを行っているので、これを利用しましょう。
    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == '/Streaming':
            global lasttime
            lasttime = time.time()
            enc = sys.getfilesystemencoding()
プレビュー画をリクエストがあったら、その時間を記憶しておきます。
def videoCapture():
    global capture
    global out
    while capture:
        nowtime = time.time()
        if nowtime - lasttime > 10:
            capture = False
            out.release()
            out = None
            break
        _, img = cap.read()
        out.write(img)
ビデオキャプチャーの周期処理の中で、現在時刻と、プレビュー画要求時の時刻を比較します。
周期処理の時刻がキャプチャー時の時刻より10秒経過していたら撮影を終了します。
ブラウザを撮影途中で閉じた場合、プレビュー画要求時の時刻が更新されなくなりますので、こうすることで、ブラウザを閉じてから10秒後に撮影は終了します。

さて、カメラでやりたいことが終わってしまった・・・
次何しようかな。
		特に特筆することはやっていないので、進捗報告だけ。
https://github.com/takishita2nd/diet-mng

とりあえず画面の表示だけ。
全チェック処理とか、テンプレートに移す処理とか、データを削除する処理とかは次回やります。
あと、管理者アカウントを別に作って、他の人がこのページにアクセスできないようにする必要もありますね。
		
画像を配置するのは簡単なんですが、
その配置位置を調整するのがめちゃくちゃ大変だったりします。
    auto Button = Sprite::create("btnChara.png");
    if (Button == nullptr)
    {
        problemLoading("'btnChara.png'");
    }
    else
    {
        Button->setPosition(Vec2(sprite->getPosition().x + sprite->getContentSize().width * scaleRate / 2 + origin.x, visibleSize.height + origin.y));
        Button->setAnchorPoint(Vec2(1.0,1.0));
        Button->setScale(3.0);
        this->addChild(Button, 1);
    }
まず、setAnchorPoint()についてですが、
これは画像位置の基準となるポイントを設定する関数ですね。
デフォルトでは、アンカーポイントは中心(0.5, 0.5)の位置にあり、これを左下にするには(0, 0)、右上にするには(1, 1)と設定します。
今回は画像の右上を、背景の右上に合わせたいので、(1.0 , 1.0)、すなわち、画像のアンカーポイントを右上に設定しています。
次に背景画像の右上の座標を調べる必要があるのですが、
getContentSize()でspriteのサイズがわかりますが、
これはsetScale()で拡大する前の値になっていました。
なので、getContentSize()の値に拡大率を計算式に入れることで、見事に位置が合いました。
あとは、ちょうど良い感じで画像の大きさを調整。
もう少し大きくしても良いかな?
用意したアイコンを全部配置して、バランスを取ってみたいと思います。
		これの続き。
仮想PCにUbuntu20.04をセットアップしたものの、
今の状態ではホストPCからでしか仮想PCに接続できないので、
これをホストPCの外からでも接続できるようにします。
仮想PCのネットワーク設定はデフォルトこうなっています。

内部ネットワークが選択されているから、外からアクセス出来ないんでしょう。
なので、新しい仮想ネットワークスイッチを追加。

接続の種類を外部ネットワークにして、物理ネットワークデバイスを設定。
これを仮想PCのネットワークに設定します。

これで仮想PCを起動。
ルータのDHCPでIPアドレスが割り当てられていれば成功。
あとは、MACアドレスを静的な値に設定、ルータのDHCP設定で固定IPアドレスを登録すればOKでしょう。
		
今使用しているモデムですが、

無線ルータ・ひかり電話内蔵のモデルなんですが、
今までは業者が全部セットアップしてくれたおかげで、設定をいじくることができませんでした。
しかし、ビッグローブにプロバイダを乗り換えたことで、設定が初期化・再設定を行ったことにより、設定をいじくることができるようになったのです。

マインクラフトサーバはメインPC内の仮想PCで動作させたいと思います。
今持っているラズパイ4では、メモリは十分でもCPUパワーが足りない。
そして、外部からマイクラサーバに接続するには、ルータの設定で仮想PCに接続できるよう設定を変える必要があるのですが。
通常のルータはポートフォワーディングといって、特定ポート番号でのアクセスのみを内部PCに転送する機能があるのですが、
このモデム一体型ルータはポート番号に関係なく、外からのアクセスを内部PCに転送する設定にしかできないみたいです。
ということは、その設定を有効にした場合、転送先のPCは常に外からのアクセスに晒されるので、ファイヤーウォールの設定は必須になりますな。
さて、実際に仮想PCを立ち上げようか。
		
今回はWebのプレビュー画面から動画の撮影を行いたいと思います。
赤丸のボタンを設置し、押すと録画開始、もう一度押すと録画停止という感じです。
まずはサーバ側。
すでに動画を撮影する方法は知っているので、これを使用します。
POSTリクエストを受け付ける処理を書いていきます。
    def do_POST(self):
        global thread
        global aviFilename
        global capture
        global out
        content_len = int(self.headers.get('content-length'))
        requestBody = json.loads(self.rfile.read(content_len).decode('utf-8'))
        if requestBody['contents']['command'] == 1:
            _, img = cap.read()
            dt_now = datetime.datetime.now()
            filename = dt_now.strftime('%Y%m%d_%H%M%S')+".jpg"
            cv2.imwrite(filename, img)
            response = {
                'status' : 200,
                'path': "http://pi4.local:8000/" + filename
            }
        if requestBody['contents']['command'] == 2:
            dt_now = datetime.datetime.now()
            aviFilename = dt_now.strftime('%Y%m%d_%H%M%S')+".avi"
            out = cv2.VideoWriter(aviFilename, fourcc, 20.0, (640,480))
            
            capture = True
            thread = threading.Thread(target=videoCapture)
            thread.start()
            response = {
                'status' : 200,
            }
        if requestBody['contents']['command'] == 3:
            capture = False
            thread.join()
            out.release()
            out = None
            response = {
                'status' : 200,
                'path': "http://pi4.local:8000/" + aviFilename
            }
        else:
            response = {
                'status' : 200
            }
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        responseBody = json.dumps(response)
        self.wfile.write(responseBody.encode('utf-8'))
録画開始(command=2)を受信した場合、VideoWriterを作成し、動画を保存するスレッドを起動します。
録画停止(command=3)を受信した場合は、このスレッドを停止するように動作します。
実際のスレッド処理はこんな感じです。
def videoCapture():
    while capture:
        _, img = cap.read()
        out.write(img)
captureフラグがTrueのときは延々とカメラの映像を動画ファイルに保存します。
録画停止時にcaptureフラグをFalseに変更し、このスレッドが終了するのをjoinで待ってから、作成された動画ファイルのパスを返信します。
動画のダウンロードですが、たぶん、content-typeを用意しないと行けないので、Dictionaryに追加しています。
CONTENT_TYPE = {'.html': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'text/javascript', '.json': 'application/json',
        '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
        '.css':'text/css', '.avi': 'video/x-msvideo'}
さて、このままだと録画中にブラウザを閉じてしまって、録画を止める手段が無くなってしまいます。
これの対処を次回やります。
(そろそろカメラネタも無くなってきた。)
		C#やJavaにはガベージコレクションと言って、使用しなくなったオブジェクトを自動的に解放してくれる仕組みがありまして、
だからnewしたオブジェクトの使用後を意識しなくても使用できるんですが、
C++にはそんな機能は無く、
newしたオブジェクトは明示的にdeleteしないと、そのオブジェクトは消えません。
で、cocos2d-xはspriteなどを作成するときにnewを使用せずにcreate()メソッドを使用しているんです。
そして、サンプルを見る限りではdeleteは行っていない。
これはcocos2d-xのプラットフォームの中で独自にガベージコレクション的な動きをしています。
ただ、cocos2d-xのガベコレとC#やJavaのガベコレとは少しロジックが異なるようです。
詳しい内容はこの記事を見て欲しく
http://furicotech.blogspot.com/2015/03/memory-management-in-cocos2d-x.html
まぁ、この内容をまとめると、
cocos2d-xのspriteなどのオブジェクトは1回使い切り。(その都度createしなければならない。)
これ、自分もコーディングしててハマりました。
あると思っていたspriteのポインタが無効なポインタになっていた、と言うことが多々ありました。
C#と同じ感覚で使用することができないんですね。
なので、クラス設計をどうするか、少し悩んでいます。
spriteなどの部品はシーンクラスの中で全部処理させた方が良いかもしれない。
		
よくスマホゲームって、タッチした場所にアニメーションを入れてエフェクトかけてるじゃないですか。
あれを実装して見たいと思います。
今回もフリー素材のぴぽや倉庫さんから素材を拝借しました。

すでに、タッチ処理のやり方もアニメーションのやり方も知っているので、これを組み合わせます。
コードはこうなりました。
bool HelloWorld::init()
{
  中略
    auto listener1 = EventListenerTouchOneByOne::create();
    listener1->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this);
    return true;
}
bool HelloWorld::onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event)
{
    auto anime = Sprite::create("pipo-btleffect007.png");
    if (anime == nullptr)
    {
        problemLoading("'pipo-btleffect007.png'");
    }
    else
    {
        this->addChild(anime, 0);
    }
    float frameHeight = anime->getContentSize().height;
    float frameWidth = anime->getContentSize().width / 14.0;
    Vector<SpriteFrame*> animFrames;
    animFrames.reserve(14);
    for(int i = 0; i < 14; i++) {
        animFrames.pushBack(SpriteFrame::create("pipo-btleffect007.png", Rect(frameWidth * i,0,frameHeight,frameWidth)));
    }
    Animation* animation = Animation::createWithSpriteFrames(animFrames, 0.02f);
    Animate* animate = Animate::create(animation);
    anime->setPosition(touch->getLocation().x, touch->getLocation().y);
    anime->runAction(animate);
    return true;
}
前回のタッチ処理でラムダ式を使いましたが、
C++のラムダ式は思った以上に使いづらいので、
普通にコールバックとして関数を定義し、マクロで登録する形にしました。
アニメーション画像は14枚横に並んでいるので、この画像を横に14等分割にクリッピングして使用しています。
あとはアニメーション画像の位置をタップ位置に設定してアニメーションを実行します。
前回はanimateをforever(無限ループ)していましたが、今回は1回きりのアニメーションなので、animateをそのまま使用しています。
どちらも親クラスがActionクラスなので、問題ありません。
		
たぶん、これが最後の仕上げ。
まずはじゃんけんの結果を判定します。
    class BattleScene : asd.Scene
    {
        private void onClickMouseShowActionResult(asd.Vector2DF pos)
        {
            var result = janken(selectedAttack, selectedDeffece);
            if(result == BattleResult.win)
            {
                _deffencePower -= _attackPower;
                if(_deffencePower <= 0)
                {
                    Singleton.GameData.BattleResultUpdate(BattleResult.win);
                    var scene = new MainScene();
                    asd.Engine.ChangeScene(scene);
                }
                _deffenceParam.Text = "戦闘力:" + _deffencePower;
            }
            else if(result == BattleResult.lose)
            {
                _attackPower -= _deffencePower;
                if (_attackPower <= 0)
                {
                    Singleton.GameData.BattleResultUpdate(BattleResult.lose);
                    var scene = new MainScene();
                    asd.Engine.ChangeScene(scene);
                }
                _attackParam.Text = "戦闘力:" + _attackPower;
            }
            _attackResult.Hide();
            _deffenceResult.Hide();
            _status = GameStatus.SelectDeffenceAction;
        }
        private BattleResult janken(BattleIcon.Icon attack, BattleIcon.Icon deffence)
        {
            switch (attack)
            {
                case BattleIcon.Icon.Gu:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.draw;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.win;
                        case BattleIcon.Icon.Par:
                            return BattleResult.lose;
                        default:
                            return BattleResult.draw;
                    }
                case BattleIcon.Icon.Choki:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.lose;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.draw;
                        case BattleIcon.Icon.Par:
                            return BattleResult.win;
                        default:
                            return BattleResult.draw;
                    }
                case BattleIcon.Icon.Par:
                    switch (deffence)
                    {
                        case BattleIcon.Icon.Gu:
                            return BattleResult.win;
                        case BattleIcon.Icon.Choki:
                            return BattleResult.lose;
                        case BattleIcon.Icon.Par:
                            return BattleResult.draw;
                        default:
                            return BattleResult.draw;
                    }
                default:
                    return BattleResult.draw;
            }
        }
今回のじゃんけん判定はベタな方法で実装しました。
上手い方法が思い浮かばなかった。
janken()でじゃんけんの結果を判定し、その結果を反映、勝負が決まったら、その情報をGameData.BattleResultUpdate()に反映し、MainSceneに遷移します。
    class GameData
    {
        public void BattleResultUpdate(BattleResult result)
        {
            if (gameStatus == GameStatus.ActionEnemy)
            {
                Battle.EnemyTurnEnd(result);
            }
            else if (gameStatus == GameStatus.ActionPlayer)
            {
                Battle.MyTurnEnd(result);
                gameStatus = GameStatus.ActionEnemy;
            }
        }
状態が敵のターンか、自分のターンかで分岐させます。
    class Battle
    {
        public void EnemyTurnEnd(BattleResult result)
        {
            if(result == BattleResult.win)
            {
                lastAttack.CombinationCity(lastDeffece);
                lastDeffece.Lose();
                aliveCities.Remove(lastDeffece);
                lastDeffece = null;
            }
        }
        public void MyTurnEnd(BattleResult result)
        {
            if (result == BattleResult.win)
            {
                lastAttack.CombinationCity(lastDeffece);
                lastDeffece.Lose();
                aliveCities.Remove(lastDeffece);
                lastDeffece.ClearPaint();
                lastDeffece = null;
            }
            lastAttack.ClearPaint();
            lastAttack = null;
            cityCnt++;
            if (cityCnt >= _cities.Count)
            {
                _cities = cityRandomReplace(aliveCities);
                aliveCities = copyCity(_cities);
                cityCnt = 0;
                turn++;
            }
        }
これでデータの反映もできたはず。
あとはもう少しブラッシュアップさせて行きます。