はじめに
お正月に↓の記事を読んだ。
“どこかで見た広告”風数式パズルゲーム『Formula Warriors』ブラウザ向けに無料公開。二択の数式を選び取り、正しいルートを突き進む
自分で作ってみるのも楽しそう、ということで作ってみた。
最近お気に入りのSiv3Dというフレームワークを使って作った。
楽しかった。
作ったもの
効果音:Springin’ Sound Stock
音楽:魔王魂
画像:ぴぽや https://pipoya.net/
画面の説明
プレイヤーは猫たち。
基本的には近くの敵を追跡して交戦する。マウスでクリックするとそこに集合させることも出来る。
上のバーはHPの合計。
四則演算の結果に従って猫が増減する(□が猫の数)。
ソースコード
# include <Siv3D.hpp> // Siv3D v0.6.13
#include <random>
#include <vector>
#include <algorithm>
#include <string>
using namespace s3d;
using namespace std;
const int width = 600;
const int height = 600;
const int h = 100;
const Vec2 npos(-1, -1);
int sign(int i)
{
if (i > 0) return 1;
else if (i < 0) return -1;
else return 0;
}
class Mob
{
private:
Texture texture;
int frame;
int df;
Font font{ 20 };
bool player;
void move(const Vec2& dpos) // dposだけ移動する
{
pos += dpos;
}
void move_to(const Vec2& pos)
{
Vec2 direction(pos - this->pos);
direction.normalize();
move(velocity * direction);
}
void get_nearest_enemy(const std::vector<Mob>& enemy, int& m_min, double& d_min)
{
int M = enemy.size();
d_min = 1e15;
for (int m = 0; m < M; m++)
{
double d = Geometry2D::Distance(this->pos, enemy[m].pos);
if (d < d_min && d>0)
{
d_min = d;
m_min = m;
}
}
}
public:
int HP;
Vec2 pos;
double velocity;
int max_damege;
Mob(const char32_t* path, const Vec2 &pos, int HP=100, double velocity=1, int max_damege=10) : texture(path), frame{ 0 }, df{ 1 }, HP(HP), pos(pos), velocity(velocity), max_damege(max_damege)
{
}
void act(std::vector<Mob>& enemy, Vec2 pos=npos)
{
int m_min;
double d_min;
get_nearest_enemy(enemy, m_min, d_min);
if (d_min < 20) // 近接
{
int damege = Random(max_damege);
font(damege).drawAt(enemy[m_min].pos + Vec2(0, -30), ColorF{ 1.0, 0.0, 0.0 });
enemy[m_min].HP -= damege;
}
if (d_min > 10 && pos==npos)
{
move_to(enemy[m_min].pos);
}
else
{
move_to(pos);
}
}
void draw()
{
double u = 1. / 3. * frame;
double v = 0;
double w = 1. / 3.;
double h = 1. / 4.;
texture.uv(u, v, w, h).drawAt(pos);
frame += df;
if (frame == 2 || frame == 0) df *= -1;
}
};
s3d::String opstring(int optype)
{
s3d::String ret;
if (optype == 0) ret = U"+";
else if (optype == 1) ret = U"-";
else if (optype == 2) ret = U"×";
else ret = U"÷";
return ret;
}
double op(double a, double b, int optype)
{
double ret;
if (optype == 0)
{
ret = a + b;
}
else if (optype == 1)
{
ret = a - b;
}
else if (optype == 2)
{
ret = a * b;
}
else
{
ret = a / b;
}
return ret;
}
void Main()
{
Window::Resize(width, height + h);
const Texture background_op{U"img/op.jpg"};
RectF rect_background_op = background_op.region(Point(0, 0));
while (!rect_background_op.leftClicked())
{
background_op.draw(Point(0, 0));
System::Update();
System::Sleep(0.01s);
}
const Audio audio{ U"bgm/battle.mp3" };
audio.play();
const Audio audio_button{ U"bgm/button.mp3" };
const Texture background{ U"img/pipo-battlebg001.jpg" };
RectF rect_background = background.region(Point(0, h));
int M = 15, M_enemy = 80;
std::vector<Mob> neko;
std::vector<Mob> enemy;
for (int m = 0; m < M; m++)
{
neko.push_back(Mob(U"img/neko.png", RandomVec2(rect_background)));
}
for (int m = 0; m < M_enemy; m++)
{
enemy.push_back(Mob(U"img/enemy.png", RandomVec2(rect_background)));
}
Font font(30, Typeface::Bold);
int t = 0;
s3d::String str_op[2];
bool disp_op = false;
bool OP = false;
double num[2];
Vec2 pos_op[2];
pos_op[0] = Vec2(10, h / 2);
pos_op[1] = Vec2(200, h / 2);
int op_type[2];
ColorF op_color[2];
for (int opp = 0; opp < 2; opp++)
{
op_color[opp] = ColorF{ 0,0,0 };
}
Font sfont(15);
int Top = 10; // 計算を表示するタイミング
int DT = 30; // 計算を表示している時間
while (System::Update())
{
background.draw(Vec2(0,h));
M = neko.size();
// 演算の表示
RectF rect_op[2];
for (int opp = 0; opp < 2; opp++)
{
rect_op[opp] = font(str_op[opp]).region(pos_op[opp]);
rect_op[opp].draw().drawFrame(1, 1, Palette::Orange);; // 演算文字列がないときは表示されない(サイズゼロなので)
}
// 演算の実行
for (int opp = 0; opp < 2; opp++)
{
if (disp_op && rect_op[opp].leftClicked() & !OP)
{
audio_button.play();
op_color[opp] = ColorF{ 1,0,0 };
OP = true;
M = op(M, num[opp], op_type[opp]);
if (M <= 1) M = 1;
int nekosize = neko.size();
if (M >= neko.size())
{
for (int m = 0; m < M - nekosize; m++)
{
neko.push_back(Mob(U"img/neko.png", RandomVec2(rect_background)));
}
}
else
{
for (int m = 0; m < nekosize - M; m++)
{
neko.pop_back();
}
}
}
font(str_op[opp]).draw(pos_op[opp], op_color[opp]);
}
if (t == Top)
{
double test = 0;
while (test < 1) // 2択のどちらかは得をするように
{
for (int opp = 0; opp < 2; opp++) // 2択の生成
{
num[opp] = Random(0.1, 5.0);
op_type[opp] = Random(0, 3);
str_op[opp] = U"□ " + opstring(op_type[opp]) + U" {:.1f}"_fmt(num[opp]);
}
test = std::max<double>(op(1, num[0], op_type[0]), op(1, num[1], op_type[1]));
}
disp_op = true;
}
if (t == Top+DT) // 演算を削除(タイムオーバー)
{
for (int opp = 0; opp < 2; opp++)
{
op_color[opp] = ColorF{ 0,0,0 };
str_op[opp] = U"";
}
t = 0;
disp_op = false;
OP = false;
}
int HPsum = 0;
for (auto& e : neko)
{
if (rect_background.leftPressed()) // クリックしているところに向かう
{
e.act(enemy, Cursor::PosF());
}
else // 近くの敵に向かう
{
e.act(enemy);
}
e.draw();
HPsum += (e.HP < 0 ? 0 : e.HP);
}
int HPsume = 0;
for (auto& e : enemy)
{
e.act(neko);
e.draw();
HPsume += (e.HP<0 ? 0 : e.HP);
}
Rect(10, 10, HPsum/10, 10).draw(Palette::Blue);
Rect(10, 30, HPsume/10, 10).draw(Palette::Red);
sfont(U"cat: {}"_fmt(neko.size())).draw(Vec2(0, 8));
// 死亡判定
neko.erase(std::remove_if(neko.begin(), neko.end(), [&](const Mob& mob) {return (mob.HP < 0); }), neko.end());
enemy.erase(std::remove_if(enemy.begin(), enemy.end(), [&](const Mob& mob) {return (mob.HP < 0); }), enemy.end());
if (neko.size() == 0 || enemy.size() == 0)
{
System::Sleep(1s);
break;
}
System::Sleep(0.1s);
t += 1;
}
const Texture background_hc(U"img/highsccore.jpg");
font = Font(40);
while(System::Update())
{
background_hc.draw();
font(U"猫の数:{}"_fmt(neko.size())).draw(Point(10, height / 2));
if (MouseL.pressed()) break;
System::Sleep(0.1s);
}
}