Skip to content

Instantly share code, notes, and snippets.

@voidproc
Last active August 29, 2023 03:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save voidproc/f712cd9487e973d3db6a8841aaaee4e0 to your computer and use it in GitHub Desktop.
Save voidproc/f712cd9487e973d3db6a8841aaaee4e0 to your computer and use it in GitHub Desktop.
時間を止める能力を使い "3秒以内" にターゲットを破壊するゲーム(試作)- OpenSiv3D Discord #game-idea のお題「1 ゲーム 3 秒」より
# include <Siv3D.hpp> // OpenSiv3D v0.6.11
// 【ルールと設定】
// 主人公は、仮想空間に侵入してから 3 秒以内にターゲット(PC)をすべて破壊しなければならない。
// 3 秒経過後、侵入者は防衛システムにより消去され、システムは初期状態にリセットされ、
// 主人公の別個体が初めからミッションをやり直すことになる。
// 主人公は「強い衝撃」を受けることにより *時間を少しだけ止められる* 能力があり、
// 時間に制約がある本ミッションでは大いに役立つと思われるが、体力に限りがあるため無限にこの能力を使うことはできない。
constexpr int StageCount = 3;
const std::array<String, StageCount> StageData = {
U"................"
U"................"
U"..I.....h....T.."
U"................"
U"......####......"
U"..P...####......"
U"......####......"
U"................"
U"..T....H........"
U"................"
U"................"
U"................"
,
U"................"
U"................"
U"..T..##..##..T.."
U".....##..##....."
U"..H..........h.."
U".##...V..v...##."
U"..H..........h.."
U".....##..##....."
U"..P..##..##..T.."
U"................"
U"................"
U"................"
,
U"................"
U"................"
U".#...#...#...#.."
U".#...#...#...#.."
U".#.T.#.T.#.T.#.."
U".#.V.#.v.#.V.#.."
U".#...#...#...#.."
U".#...#...#...#.."
U".......P........"
U".I.....I.....I.."
U"................"
,
};
// ゲーム全体にわたって共有されるデータ
struct GameData
{
int stage = 0;
VariableSpeedStopwatch clock{ StartImmediately::Yes };
Array<Optional<double>> result;
};
// App
using App = SceneManager<String, GameData>;
// タイトルシーン
class TitleScene : public App::Scene
{
public:
TitleScene(const InitData& init)
: IScene{ init }
{
}
void update() override
{
if (KeyEnter.down() && not timeEnter_.isRunning())
{
timeEnter_.start();
}
if (timeEnter_.isRunning() && timeEnter_.sF() > 1.0)
{
changeScene(U"MainScene", 0ms);
}
}
void draw() const override
{
FontAsset(U"title")(U"MISSION IN 3SEC").drawAt(64, Scene::CenterF());
const double alpha = timeEnter_.isRunning() ? Periodic::Pulse0_1(0.15s, 0.65) : 1.0;
FontAsset(U"title")(U"Press Enter Key").drawAt(32, Scene::CenterF().movedBy(0, 64), ColorF{ 1.0, alpha });
}
private:
Stopwatch timeEnter_;
};
// 壁
class Wall
{
public:
Wall(const RectF& rect)
: rect_{ rect }
{
}
void draw() const
{
rect_
.rounded(8)
.draw(Palette::Gray)
.drawFrame(3.0, Palette::Silver);
}
const RectF collision() const
{
return rect_;
}
private:
RectF rect_;
};
enum class Direction
{
Left,
Right,
Front,
Back,
};
// プレイヤー
class Enemy;
class Target;
class Item;
class Player
{
public:
Player(const Vec2& defaultPos)
: pos_{ defaultPos }
{
}
void update(const Array<Wall>& walls)
{
if (not alive_) return;
constexpr double MaxSpeed = 200.0;
const double DeltaSpeed = 500.0 * Scene::DeltaTime();
if (KeyLeft.pressed())
{
dir_ = Direction::Left;
speed_.x -= DeltaSpeed;
}
else if (KeyRight.pressed())
{
dir_ = Direction::Right;
speed_.x += DeltaSpeed;
}
if (KeyUp.pressed())
{
speed_.y -= DeltaSpeed;
}
else if (KeyDown.pressed())
{
speed_.y += DeltaSpeed;
}
// 減速
speed_.setLength(Max(speed_.length() - speed_.length() * 2 * Scene::DeltaTime(), 0.0));
speed_.limitLengthSelf(MaxSpeed);
pos_ += speed_ * Scene::DeltaTime();
for (const auto& wall : walls)
{
if (collision().intersects(wall.collision()))
{
pos_ -= speed_ * Scene::DeltaTime() * 2;
speed_ = -speed_;
break;
}
}
}
template <class Obj>
void onCollide(Obj& obj)
{
if constexpr (std::is_same_v<Obj, Enemy>)
{
// 無敵になる
timerInvincible_.restart(1.5s);
// 体力が減る
life_ = Clamp(life_ - 34.0, 0.0, MaxLife);
if (life_ <= 1e-3)
{
kill();
}
// スピードが0になる
speed_ = Vec2::Zero();
}
else if constexpr (std::is_same_v<Obj, Target>)
{
}
else if constexpr (std::is_same_v<Obj, Item>)
{
// 体力が回復する
life_ = Clamp(life_ + 20.0, 0.0, MaxLife);
}
}
void draw() const
{
if (timeKilled_.sF() < 1.0)
{
const double alphaKilled = timeKilled_.isRunning() ? Periodic::Square0_1(0.1s) : 1.0;
const double alpha = timerInvincible_.isRunning() ? 0.4 + 0.5 * Periodic::Square0_1(0.1s) : 1;
// プレイヤーのダメージエフェクト
if (timerInvincible_.isRunning() && timerInvincible_.sF() > 1.2)
{
Shape2D::NStar(16, 56, 36, pos_).draw(ColorF{ Palette::Whitesmoke, (0.8 * Periodic::Square0_1(0.1s)) });
}
// ダメージ時画面赤フラッシュ
if (timerInvincible_.isRunning() && timerInvincible_.sF() > 0)
{
const double t = (timerInvincible_.sF() - 0) / 1.5;
Scene::Rect().draw(ColorF{ Palette::Red, 0.5 * EaseInExpo(t) });
}
TextureAsset(U"player")
.mirrored(dir_ == Direction::Right)
.resized(72)
.rotated(20_deg * Sin(Scene::Time() * 8.0))
.drawAt(pos_, ColorF{ 1, alpha * alphaKilled });
}
// 体力ゲージ
const auto gaugeRect = RectF{ Arg::center = Scene::Rect().bottomCenter().movedBy(0, -48), SizeF{ 600, 32 } };
gaugeRect
.rounded(32)
.draw(Palette::Red);
if (life_ > 0.1)
gaugeRect
.rounded(32)
.setSize(600 * life_ / 100, 32)
.draw(Palette::White);
}
Circle collision() const
{
return Circle{ pos_, 56 / 2 };
}
template <class Obj>
bool isCollidedTo(const Obj& obj) const
{
if (not alive_) return false;
if constexpr (std::is_same_v<Obj, Enemy>)
{
if (timerInvincible_.isRunning()) return false;
}
return collision().intersects(obj.collision());
}
const Vec2& pos() const
{
return pos_;
}
const double life() const
{
return life_;
}
void kill()
{
alive_ = false;
timeKilled_.start();
}
bool alive() const
{
return alive_;
}
private:
// 位置
Vec2 pos_{};
// 向き
Direction dir_ = Direction::Right;
// 速度
Vec2 speed_{};
// 無敵
Timer timerInvincible_{};
// 生きてるか(衝突判定をするか)
bool alive_ = true;
// 破壊された
Stopwatch timeKilled_;
// 体力
inline static constexpr double MaxLife = 100.0;
double life_ = MaxLife;
};
// 敵
enum class EnemyType
{
BeeH,
BeeH2,
BeeV,
BeeV2,
};
class Enemy
{
public:
Enemy(const GameData& gameData, EnemyType type, const Vec2& defaultPos)
: gameData_{ gameData }, type_{ type }, defaultPos_ { defaultPos }, pos_{ defaultPos }
{
}
void update()
{
Vec2 motion{};
switch (type_)
{
case EnemyType::BeeH:
motion = Vec2{ 100.0, 0.0 };
break;
case EnemyType::BeeH2:
motion = Vec2{ -100.0, 0.0 };
break;
case EnemyType::BeeV:
motion = Vec2{ 0.0, 85.0 };
break;
case EnemyType::BeeV2:
motion = Vec2{ 0.0, -85.0 };
break;
}
const auto pos = defaultPos_ + motion * Periodic::Sine1_1(2.0s, gameData_.clock.sF());
speed_ = pos - pos_;
pos_ = pos;
}
void draw() const
{
TextureAsset(U"enemy")
.mirrored(speed_.x > 0)
.resized(64)
.drawAt(pos_ + Vec2{ 0, 16.0 } * Periodic::Sine1_1(0.7s, gameData_.clock.sF()));
}
Circle collision() const
{
return Circle{ pos_, 56 / 2 };
}
private:
const GameData& gameData_;
// 種類
EnemyType type_;
// 位置
Vec2 defaultPos_{};
Vec2 pos_{};
// 速度
Vec2 speed_{};
};
// 目標物
class Target
{
public:
Target(const Vec2& defaultPos)
: pos_{ defaultPos }
{
}
void update()
{
}
void draw() const
{
if (timeKilled_.sF() > 1.0) return;
const double alpha = timeKilled_.isRunning() ? Periodic::Square0_1(0.1s) : 1.0;
TextureAsset(U"target")
.resized(64)
.drawAt(pos_, ColorF{ 1.0, alpha });
}
Circle collision() const
{
return Circle{ pos_, 56 / 2 };
}
void kill()
{
alive_ = false;
timeKilled_.start();
}
bool alive() const
{
return alive_;
}
private:
// 位置
Vec2 pos_{};
// 生きてるか(衝突判定をするか)
bool alive_ = true;
// 破壊された
Stopwatch timeKilled_;
};
// アイテム
class Item
{
public:
Item(const Vec2& defaultPos)
: defaultPos_{ defaultPos }, pos_{ defaultPos }
{
}
void update()
{
}
void draw() const
{
if (timeKilled_.sF() > 1.0) return;
const double alpha = timeKilled_.isRunning() ? Periodic::Square0_1(0.1s) : 1.0;
TextureAsset(U"item")
.resized(64)
.drawAt(pos_ + Vec2{ 0.0, 4.0 } * Periodic::Sine1_1(1.1s), ColorF{ 1, alpha });
}
Circle collision() const
{
return Circle{ pos_, 56 / 2 };
}
void kill()
{
alive_ = false;
timeKilled_.start();
}
bool alive() const
{
return alive_;
}
private:
// 位置
Vec2 defaultPos_{};
Vec2 pos_{};
// 生きてるか(衝突判定をするか)
bool alive_ = true;
// 破壊された
Stopwatch timeKilled_;
};
// メインシーン
class MainScene : public App::Scene
{
public:
MainScene(const InitData& init)
: IScene{ init }
{
if (getData().stage >= StageData.size())
{
getData().stage = 0;
}
if (getData().stage == 0)
{
getData().result.clear();
for (int iStage : step(StageCount))
{
getData().result.push_back(none);
}
}
for (auto [index, c] : Indexed(StageData[getData().stage]))
{
const Vec2 objPos{ (index % 16) * 50.0 + 25.0, (index / 16) * 50.0 + 25.0 };
if (c == U'H')
{
enemies_.emplace_back(getData(), EnemyType::BeeH, objPos);
}
else if (c == U'h')
{
enemies_.emplace_back(getData(), EnemyType::BeeH2, objPos);
}
else if (c == U'V')
{
enemies_.emplace_back(getData(), EnemyType::BeeV, objPos);
}
else if (c == U'v')
{
enemies_.emplace_back(getData(), EnemyType::BeeV2, objPos);
}
else if (c == U'T')
{
targets_.emplace_back(objPos);
}
else if (c == U'I')
{
items_.emplace_back(objPos);
}
else if (c == U'#')
{
walls_.emplace_back(RectF{ objPos - Vec2{ 25.0, 25.0 }, 50.0 });
}
else if (c == U'P')
{
player_ = Player{ objPos };
}
}
}
void update() override
{
// ゲーム開始のカウントダウン
if (not time_.isStarted() && timerCountdown_.reachedZero())
{
timerCountdown_.reset();
time_.start();
getData().clock.restart();
}
// ゲーム中
if (time_.isRunning())
{
// キャラクターの更新
player_.update(walls_);
for (auto& enemy : enemies_)
{
enemy.update();
}
for (auto& target : targets_)
{
target.update();
}
for (auto& item : items_)
{
item.update();
}
// 衝突判定
for (auto& enemy : enemies_)
{
if (player_.isCollidedTo(enemy))
{
player_.onCollide(enemy);
// 画面シェイク
quakeAmount_.y = 20;
// 6秒間スローになる
timerSlow_.restart(6s);
break;
}
}
for (auto& target : targets_)
{
if (player_.isCollidedTo(target) && target.alive())
{
player_.onCollide(target);
target.kill();
// 画面シェイク
quakeAmount_.y = 10;
// 最後の 1 個を kill したらタイマーストップ
// 次のステージへ(遷移するためのタイマーをセット)
if (targets_.count_if([](const auto& t) { return t.alive(); }) == 0)
{
time_.pause();
timerSlow_.reset();
timeAllTargetDestroyed_.start();
getData().clock.setSpeed(1);
}
}
}
for (auto& item : items_)
{
if (player_.isCollidedTo(item) && item.alive())
{
player_.onCollide(item);
item.kill();
}
}
// タイムアップ
if (timeRemain_() <= 1e-3)
{
player_.kill();
}
// プレイヤー生きてるか?
// しんでたらゲームオーバーへ
if (not player_.alive() && not timePlayerKilled_.isRunning())
{
timePlayerKilled_.start();
timerSlow_.reset();
getData().clock.setSpeed(1);
}
}
// スロー
if (timerSlow_.isRunning())
{
const double spd = EaseInExpo(timerSlow_.progress0_1());
time_.setSpeed(spd);
getData().clock.setSpeed(spd);
}
// 画面シェイク
quakeAmount_.x -= 20.0 * Scene::DeltaTime() / 0.3;
quakeAmount_.x = Max(quakeAmount_.x, 0.0);
quakeAmount_.y -= 20.0 * Scene::DeltaTime() / 0.3;
quakeAmount_.y = Max(quakeAmount_.y, 0.0);
// ステージクリアしたのでキー入力待ち
if (timeAllTargetDestroyed_.sF() > 1.5)
{
if (KeyEnter.down())
{
// 次のステージへ
getData().result[getData().stage] = timeRemain_();
getData().stage += 1;
if (getData().stage >= StageCount)
changeScene(U"ResultScene", 0ms);
else
changeScene(U"MainScene", 0ms);
}
}
// ゲームオーバー
// 入力待ち
if (timePlayerKilled_.sF() > 3.0)
{
if (KeyEnter.down())
{
getData().stage = 0;
changeScene(U"ResultScene", 0ms);
}
}
}
void draw() const override
{
// 画面シェイク
const Transformer2D transformerQuake(Mat3x2::Translate(quakeAmount_.x * Periodic::Sine1_1(0.1s), quakeAmount_.y * Periodic::Sine1_1(0.07s)));
// 背景
for (int iy : step(600 / 50 + 4))
{
for (int ix : step(800 / 50 + 4))
{
RectF{ (ix - 2) * 50, (iy - 2) * 50, 50 }.draw(ColorF{ 0.18, 0.18, 0.39 }).drawFrame(1.0, 0, ColorF{ Palette::Slateblue, 0.5 });
}
}
// プレイヤー
player_.draw();
// 壁
for (const auto& wall : walls_)
{
wall.draw();
}
// 敵
for (const auto& enemy : enemies_)
{
enemy.draw();
}
// ターゲット
for (const auto& target : targets_)
{
target.draw();
}
// アイテム
for (const auto& item : items_)
{
item.draw();
}
// ゲーム開始のカウントダウン表示
if (timerCountdown_.isRunning())
{
Scene::Rect().draw(ColorF{ 0, 0.8 });
const auto countdownText = timerCountdown_.s() < 1 ? U"GO!" : U"Ready";
FontAsset(U"countdown")(countdownText).drawAt(168, Scene::CenterF(), Palette::White);
}
// スロー
if (timerSlow_.isRunning())
{
const double r = 1000 * EaseOutExpo(timerSlow_.progress1_0());
const double alpha = timerSlow_.progress1_0();
Circle{ player_.pos(), r }
.draw(ColorF{ Palette::Lime, 0.1 + 0.2 * EaseInSine(alpha) });
}
// 残り時間
if (time_.isStarted())
{
const ColorF clearTimeColor = ColorF{ Palette::Yellow, 0.7 + 0.3 * Periodic::Pulse0_1(0.3s, 0.8) };
const ColorF timeRemainColor = timerSlow_.isRunning() ? Palette::Lime.lerp(Palette::White, timerSlow_.progress0_1()) : Palette::White;
const double alpha = timerSlow_.isRunning() ? 0.3 + 0.7 * Periodic::Square0_1(0.2s) : 1.0;
const bool cleared = time_.isPaused();
FontAsset(U"time_remain")(U"{}{:.2f}{}"_fmt(cleared ? U"Clear! " : U"", timeRemain_(), cleared ? U"s" : U""))
.drawAt(80, Scene::Rect().topCenter().movedBy(0, 56), cleared ? clearTimeColor : ColorF{ timeRemainColor, alpha });
}
// ステージ名とか
const auto stageText = FontAsset(U"stage")(U"Stage {}"_fmt(getData().stage + 1));
const auto stageTextRegion = stageText.region(24);
stageText.draw(24, Scene::Rect().br().movedBy(-stageTextRegion.size).movedBy(-8, 0), Palette::White);
// ステージクリアしたのでキー入力待ち
if (timeAllTargetDestroyed_.sF() > 1.5)
{
Scene::Rect().draw(ColorF{ 0, 0.5 });
FontAsset(U"title")(U"Press Enter Key").drawAt(Scene::CenterF(), ColorF{ 1, 0.7 + 0.3 * Periodic::Square0_1(0.3s)});
}
// ゲームオーバー
// 入力待ち
if (timePlayerKilled_.sF() > 3.0)
{
Scene::Rect().draw(ColorF{ Palette::Red, 0.5 });
FontAsset(U"title")(U"GAME OVER").drawAt(96, Scene::CenterF(), ColorF{ 1 });
}
}
private:
// メインシーンの経過時間
VariableSpeedStopwatch time_;
// メインシーンの残り時間
double timeRemain_() const
{
return Max(0.0, 3.0 - time_.sF());
}
// カウントダウン
Timer timerCountdown_{ 2s, StartImmediately::Yes };
// プレイヤー
Player player_{ Vec2{ 200, 300 } };
// 敵
Array<Enemy> enemies_{};
// ターゲット
Array<Target> targets_{};
// アイテム
Array<Item> items_{};
// 壁
Array<Wall> walls_{};
// 画面シェイク
Vec2 quakeAmount_{};
// スロー
Timer timerSlow_;
// ステージクリアした
Stopwatch timeAllTargetDestroyed_;
// プレイヤーがしんだ
Stopwatch timePlayerKilled_;
};
// リザルトシーン
class ResultScene : public App::Scene
{
public:
ResultScene(const InitData& init)
: IScene{ init }
{
}
void update() override
{
if (timeEnter_.sF() > 2.0 && KeyEnter.down())
{
changeScene(U"MainScene", 0ms);
}
}
void draw() const override
{
// 背景
for (int iy : step(600 / 50 + 4))
{
for (int ix : step(800 / 50 + 4))
{
RectF{ (ix - 2) * 50, (iy - 2) * 50, 50 }.draw(ColorF{ 0.10, 0.10, 0.25 }).drawFrame(1.0, 0, ColorF{ Palette::Slateblue, 0.3 });
}
}
constexpr double LineHeight = 72;
constexpr double h = LineHeight * StageCount;
for (int iStage : step(StageCount))
{
FontAsset(U"stage")(U"Stage {} : {}"_fmt(iStage + 1, getData().result[iStage].has_value() ? U"{:.2f} sec"_fmt(getData().result[iStage].value()) : U"No data"))
.drawAt(56, Scene::CenterF().movedBy(0, iStage * LineHeight - h / 2 + LineHeight / 2));
}
}
private:
Stopwatch timeEnter_{ StartImmediately::Yes };
};
void Main()
{
Scene::SetBackground(Palette::Mediumorchid);
// アセット
TextureAsset::Register(U"player", U"🏃‍♂️"_emoji);
TextureAsset::Register(U"enemy", U"🐝"_emoji);
TextureAsset::Register(U"target", U"🖥️"_emoji);
TextureAsset::Register(U"item", U"🍬"_emoji);
FontAsset::Register(U"title", FontMethod::MSDF, 48, Typeface::Black);
FontAsset::Register(U"countdown", FontMethod::MSDF, 48, Typeface::Black);
FontAsset::Register(U"time_remain", FontMethod::MSDF, 48, Typeface::Heavy, FontStyle::Italic);
FontAsset::Register(U"stage", FontMethod::MSDF, 48, Typeface::Heavy);
// シーン登録
App app;
//app.add<TitleScene>(U"TitleScene");
app.add<MainScene>(U"MainScene");
app.add<ResultScene>(U"ResultScene");
app.setFadeColor(Palette::Plum);
app.changeScene(U"MainScene", 0ms);
while (System::Update())
{
if (not app.update())
{
break;
}
}
}
@voidproc
Copy link
Author

demo.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment