こちらのサイトに無限繁殖機構が紹介されていました。
https://wikiwiki.jp/craftopia/
リンク先は絵になってわかりづらいと思いますが、
要は、このように配置するみたいです。
矢印はコンベアーの動く向きです。
こうすることで牛が繁殖機に集まることになり、
自ら繁殖機の上に乗ってくれます。
上に積み重なっていくのは何でだろうねぇ?
もうちょっと細工すれば溢れた動物を処理できるかも。

こちらのサイトに無限繁殖機構が紹介されていました。
https://wikiwiki.jp/craftopia/
リンク先は絵になってわかりづらいと思いますが、
要は、このように配置するみたいです。

矢印はコンベアーの動く向きです。
こうすることで牛が繁殖機に集まることになり、
自ら繁殖機の上に乗ってくれます。

上に積み重なっていくのは何でだろうねぇ?
もうちょっと細工すれば溢れた動物を処理できるかも。
前回までの状況はこちら。
はい、大幅に書き換えました。
詳細はgitHubのソースを見て欲しいのですが、
https://github.com/takishita2nd/HokkaidoWar
まず、メインシーンで使用していたデータは全てGameDataクラスに移動しました。
class GameData
{
public enum GameStatus
{
None,
SelectCity,
ActionEnemy,
ActionPlayer,
ShowResult,
GameEnd,
GameOver
}
public GameStatus gameStatus = GameStatus.None;
public List<City> Cities = null;
public List<City> AliveCities = null;
public Battle Battle = null;
public Player Player = null;
このクラスはシングルトンで管理します。
class Singleton
{
private static GameData _gameData = null;
public static GameData GetGameData()
{
if (_gameData == null)
{
_gameData = new GameData();
}
return _gameData;
}
このシングルトンクラス、ゲッターのでいいんじゃないかと思い始めた。
やっていることは同じですが。
メソッドでやるか、プロパティでやるかの違いです。
気が向いたら直します。
シーン遷移は全てシーンクラスのインスタンスを作り直します。
if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
{
var scene = new MainScene();
asd.Engine.ChangeScene(scene);
}
そのときに、ゲームデータを元に画面を作り直す、という感じです。
これで、バトルシーンからメインシーンに切り替わっても、ゲームが止まることは無くなりました。
これでやっと次に進める・・・。
繁殖機という設備がクラフトできたので、使ってみました。

繁殖機の上に赤い部分が二つあるので、ここに動物を運ぶと、繁殖機が動物を産みだしてくれます。
とは言っても無限ではないので、繁殖元の動物が死ぬと、繁殖機の動きが止まります。
あと、動物を運ぶのには、モンスタープリズムというアイテムが必要です。

はい、あのポケモンのあの丸いボールのような物です。
ある程度ダメージを与えて、このボールをぶつけると動物を捕まえることができ、好きなところに出現させることができます。

とりあえず、逃げないように囲って、繁殖場を作ってみたのですが、
勝手に動物が繁殖機に乗ってくれるような仕組みって無いんですかね?
あと、繁殖させた動物を処理する仕組みも考えてみたのですが、

コンベアーが燃えてしまいました。
壁も木材使うと燃えるので、これもダメですね。
コンベアーで上に運んで焚き火の上に落下させる方法が良いのかもしれない(確か、まだ作れない)
そろそろ次の文明に発展させるか・・・
前回作ったバトルシーンを、既存の状態遷移に組み込んでみました。

変更前はこんな感じでした。

結構複雑になってきましたね。
でも修正はそんなに難しくない・・・と思いきや。
どうやらシーンは切り替えるごとに作り直さなくちゃいけないらしくて
バトルシーンからメインシーンに切り替わるときに、OnUpdated()処理が走らないという。
おそらくシーンのプロパティにIsAliveというのがあって、

これはReadOnlyのプロパティなので、やっぱりシーンを作り直さなくちゃいけないんだろうなぁ、と思います。
ということは、ゲームデータはシーンとは別に保持しなくちゃいけなくて、
大幅に設計変更が発生しそう、
と分かった時に心が折れました。
まぁ、完成はさせたいので、時間があるときに修正しておきます。
というか、メリットしかない。
と言うのも、自分が過去にやらかしているから。
自分が東京で仕事していた頃、
東京都内は家賃が高いから、家賃の安い千葉県松戸市に住んでいました。
当時、通勤時間2時間かかってました。
朝定時に出社するために朝5時に起床し、終電まで仕事して家に着く頃には午前1時、という生活を当たり前のように送っていました。
当然、こんな職場にも問題はありましたが、
とにかく睡眠時間が取れないのがきつかった。
直近の職場、札幌では朝8時過ぎて家を出ても出社時間の9時には十分間に合っていたので、前者に比べれば歴然の差。
むしろ加齢で早く目が覚めてしまう方がきつかった。
とにかく、睡眠時間を確保するためにも、多少高くても狭くても職場に近い所に住むべき。
ということを学びました。
もう一つ。
東京って、満員電車がきつかった。
出社するだけで疲れてる。
長時間の満員電車で体力消耗するくらいなら、短い方が遙かに良い。
なんなら、電車使わなくても通勤できる方がいい。
東京でコロナの感染経路不明の感染者って、たぶん、電車の中で感染していると思います。
コロナ対策の面でも、通勤時間は減らすべき。
電車内で本読んだり勉強する時間が無い?
勉強は家でやれば良いだろう。
家を出る時間が遅くなる分勉強する時間は確保できているハズだ。
なんなら早く家を出て、おしゃれな喫茶店で朝活するのもいいだろう。電車の中で勉強するより遙かに快適だ。
とにかく、通勤で体を壊すという事態にはならないでもらいたい。

前回はコマンドでカメラを操作しましたが、
今回はプログラムからカメラを使用します。
やり方は2種類あるみたいで、
python-picameraを使用する方法と、
openCVを使用する方法です。
今回はopenCVを使用してみます。
なぜなら、openCVは画像解析なんかもできるので、今後も応用が利くかも、と思いましたので。
$ sudo apt-get install python3-opencv
pip3を使用するインストール方法もネットにありましたが、
こちらはインストールの過程でビルド処理がありますので、
実行すると、一日コースです。
apt-getを使用したほうが短時間で完了します。
(それでも数分かかります。)
from datetime import datetime
import cv2, os
def main():
cam = cv2.VideoCapture(0)
if cam == None:
return False
while True:
# カメラから映像を読み込む
_, img = cam.read()
cv2.imshow("preview", img)
# キーを入力した際に撮影を終了する
if cv2.waitKey(1) < 255: break
# 事後処理
cam.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
cam.read()を実行すると、処理結果と、frameデータが取得できます。
ここでは処理結果は使用しないので、_ と書いています。
imshow()を実行するとframeのデータを画面に表示されます。
それをwhileループしているので、プレビューのように映像が表示されます。
※ラズパイの画面をVNCのリモートデスクトップで表示させています。
最終的には、このframeデータをリモートに飛ばして表示させたい。
openCVは調べてみると、いろいろな事ができるみたいなので、もうちょっと調べてみます。
前回までの状況はこちら

最新ソースはこちら(gitHub)。
https://github.com/takishita2nd/diet-mng
前回計算した内容をデータベースに保存・読み出しを行う機能を実装します。
まずはデータベースのマイグレーションを作成。
class CreateEatingTarget extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('eating_targets', 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_target_user', function (Blueprint $table) {
$table->integer('user_id')
->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
$table->integer('eating_target_id')
->foreign('eating_target_id')
->references('id')->on('eating_targets')
->onDelete('cascade');
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('eating_target_user');
Schema::dropIfExists('eating_targets');
}
}
$ php artisan migrate
このテーブルのモデルを作成。
class EatingTarget extends Model
{
protected $table = 'eating_targets';
public function users()
{
return $this->belongsToMany('App\User');
}
}
Userテーブルからのリレーションも作成します。
class User extends Authenticatable
{
public function EatingTargets()
{
return $this->belongsToMany('App\Model\EatingTarget');
}
このデータベースにアクセスするためのAPIを作成します。
セット処理。
Route::post('api/eating/settarget', 'Eating\ApiController@setTarget');
class ApiController extends Controller
{
/**
* 目標栄養素を設定する
*/
public function setTarget(Request $request)
{
$paramNames = $this->eatingManagement->getTargetParam();
$param = [];
foreach($paramNames as $name) {
$param[$name] = $request->contents[$name];
}
$this->eatingManagement->setTarget($param, Auth::user());
return response()->json();
}
use App\Model\EatingTarget;
class EatingManagementRepository
{
private $targetParamNames = ['protein', 'liqid', 'carbo', 'calorie'];
public function setTarget($param, $user)
{
$model = $user->EatingTargets()->first();
if(is_null($model)) {
$model = new EatingTarget();
}
foreach($this->targetParamNames as $name)
{
$model->$name = $param[$name];
}
$model->save();
$this->attachToUser($model, $user);
}
public function getTargetParam()
{
return $this->targetParamNames;
}
これをVue.jsの処理に組み込みます。
methods: {
clickAdd: function() {
var self = this;
this.param.contents = this.contents;
axios.post('/api/eating/settarget', this.param).then(function(response){
self.closeModal();
self.$emit('update');
}).catch(function(error){
self.error_flg = true;
self.errors = error.response.data.errors;
});
},

次は、このデータを読み出す処理を実装します。
グラフデータを読み出すAPIがすでにありますので、これにデータを追加します。
class ApiController extends Controller
{
/**
* グラフ用データを取得する
*/
public function graph(Request $request)
{
return response()->json([
'data' => $this->eatingManagement->getDaily(Auth::user(), $request->contents['date']),
'target' => $this->eatingManagement->getTarget(Auth::user())
]);
}
class EatingManagementRepository
{
public function getTarget($user)
{
return $user->EatingTargets()->first();
}
graphUpdate: function() {
var ctx = document.getElementById("eating");
var self = this;
this.contents.date = this.todayDate;
this.param.contents = this.contents;
this.datasets = [];
axios.post('api/eating/graph', this.param).then(function(response){
if(response.data.data != null) {
self.datasets.push(Math.ceil(response.data.data.protein / response.data.target.protein * 100));
self.datasets.push(Math.ceil(response.data.data.liqid / response.data.target.liqid * 100));
self.datasets.push(Math.ceil(response.data.data.carbo / response.data.target.carbo * 100));
self.datasets.push(Math.ceil(response.data.data.calorie / response.data.target.calorie * 100));
var myChart = new Chart(ctx, {

なかなかいい感じなので、これで行きましょう。
今日はもう一個やったので、まとめておく。
シーンを追加する場合。
Cocos2d-xでは1シーンにつき1クラスと考えまして、
今回はSampleSceneを追加してみます。

C++の場合はクラスを記入したヘッダーファイルと、処理本体を記述したソースファイル本体を作成します。

中身は、とりあえずHelloWorldとほぼ同じにして、最初にSampleSceneを表示するようにします。
class SampleScene : public cocos2d::Scene
{
public:
static cocos2d::Scene* createScene();
virtual bool init();
// a selector callback
void menuCloseCallback(cocos2d::Ref* pSender);
// implement the "static create()" method manually
CREATE_FUNC(SampleScene);
};
#include "SampleScene.h"
#include "SimpleAudioEngine.h"
USING_NS_CC;
Scene* SampleScene::createScene()
{
return SampleScene::create();
}
// Print useful error message instead of segfaulting when files are not there.
static void problemLoading(const char* filename)
{
printf("Error while loading: %s\n", filename);
printf("Depending on how you compiled you might have to add 'Resources/' in front of filenames in HelloWorldScene.cpp\n");
}
// on "init" you need to initialize your instance
bool SampleScene::init()
{
//////////////////////////////
// 1. super init first
if ( !Scene::init() )
{
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
// add "HelloWorld" splash screen"
auto sprite = Sprite::create("HelloWorld.png");
if (sprite == nullptr)
{
problemLoading("'HelloWorld.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);
}
return true;
}
void SampleScene::menuCloseCallback(Ref* pSender)
{
//Close the cocos2d-x game scene and quit the application
Director::getInstance()->end();
/*To navigate back to native iOS screen(if present) without quitting the application ,do not use Director::getInstance()->end() as given above,instead trigger a custom event created in RootViewController.mm as below*/
//EventCustom customEndEvent("game_scene_close_event");
//_eventDispatcher->dispatchEvent(&customEndEvent);
}
// create a scene. it's an autorelease object
auto scene = SampleScene::createScene();
// run
director->runWithScene(scene);
return true;
}
ただ、C言語やっている人なら分かると思いますが、これだけじゃあビルドしてくれません。
通常はmakefileを弄るのですが、WindowsはVisualStudio、AndroidはAndroid Studioでビルドするので、
VisualStudioの場合
プロジェクト名\proj.win32\プロジェクト名.vcxproj
のここに追加するファイルを記載する。
<ItemGroup>
<ClCompile Include="..\Classes\AppDelegate.cpp" />
<ClCompile Include="..\Classes\HelloWorldScene.cpp" />
<ClCompile Include="..\Classes\SampleScene.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Classes\AppDelegate.h" />
<ClInclude Include="..\Classes\HelloWorldScene.h" />
<ClInclude Include="..\Classes\SampleScene.h" />
<ClInclude Include="main.h" />
</ItemGroup>
Androidの場合
プロジェクト名\CMakeLists.txt
のここに追加するファイルを記載する。
# add cross-platforms source files and header files
list(APPEND GAME_SOURCE
Classes/AppDelegate.cpp
Classes/HelloWorldScene.cpp
Classes/SampleScene.cpp
)
list(APPEND GAME_HEADER
Classes/AppDelegate.h
Classes/HelloWorldScene.h
Classes/SampleScene.h
)
Mac/iOSはXcodeのメニューから追加すれば良いと思う。(知らんけど)
こういうシーンの追加って、cocosコマンドでなんとかできないんですかね?
シーンに限らず、クラス追加するときも同じか。
めんどくさい。
前回の記事でシーンの内容は大体分かった。
ちょっと気になるのはvisibleSizeとoriginの値。
じゃあ、visibleSizeとoriginの値をラベルに表示させちゃおう。
auto str = String();
str.appendWithFormat("visible (%f %f)", visibleSize.width, visibleSize.height);
auto label = Label::createWithTTF(str.getCString(), "fonts/msgothic.ttc", 24);
if (label == nullptr)
{
problemLoading("'fonts/msgothic.ttc'");
}
else
{
// position the label on the center of the screen
label->setPosition(Vec2(origin.x + visibleSize.width/2,
origin.y + visibleSize.height - label->getContentSize().height));
// add the label as a child to this layer
this->addChild(label, 1);
}
auto str2 = String();
str2.appendWithFormat("origin (%f %f)", origin.x, origin.y);
auto label2 = Label::createWithTTF(str2.getCString(), "fonts/msgothic.ttc", 24);
if (label2 == nullptr)
{
problemLoading("'fonts/msgothic.ttc'");
}
else
{
// position the label on the center of the screen
label2->setPosition(Vec2(origin.x + visibleSize.width/2,
origin.y + visibleSize.height - label->getContentSize().height * 2));
// add the label as a child to this layer
this->addChild(label2, 2);
}
フォントについて
フォントはResource/fontsフォルダに拡張子ttfファイルが置いてあると思いますが、
Label::createWithTTF()をコールするときにフォントファイルを指定します。
Windowsだったらフォントはc:\windows\fontsがあるので、ここにあるttf/ttcファイルをここに置けば使用することができます。

文字列について
文字列はcocos2dx::Stringというクラスが存在するらしい。
これを使った方がいろいろと便利なので、これを使用する。
addChild()の第二パラメータ
これはzIndexとあったので、重ねて表示する場合、上に表示する順番を示すパラメータですね。
数字が大きい方が上に表示されるみたいです。
Windowsでの表示結果。

visibleは画面のサイズ、originは原点の座標のようです。
Windowsはこれでいいのですが、Android(pixel4a)の場合はこうなりました。

Pixel4aはちょっと横長なので、heightが少し小さいようです。
アスペクト比が異なり、アスペクト比は長辺が基準なので、その分heightが小さいのですね。
あと、左下にピンホールカメラがあるので、その分だけ、originのy座標が少し上になっていますね。
特殊ディスプレイ、嫌い。
まぁ、この点はどうするか後で考えよう。