Skip to content

Instantly share code, notes, and snippets.

@hattori8
Last active November 5, 2023 04:45
Show Gist options
  • Save hattori8/edc6ed75ec83b563f06933da2991973c to your computer and use it in GitHub Desktop.
Save hattori8/edc6ed75ec83b563f06933da2991973c to your computer and use it in GitHub Desktop.

プロジェクトの構造

ProjectDirectory/
│
├── Main.cpp
├── StateMachine.hpp
├── CharacterStates.hpp
└── CharacterStateMachine.cpp
#ifndef CHARACTERSTATEMACHINE_HPP
#define CHARACTERSTATEMACHINE_HPP
#include "StateMachine.hpp"
#include "CharacterStates.hpp"
class CharacterStateMachine : public StateMachine<CharacterState> {
private:
Stopwatch stopwatch;
std::unordered_map<CharacterState, int32> stateTextSpeeds;
CharacterState previousState;
String currentStateText;
String characterName = U"ヒカリ";
bool showCharacterName = false;
void resetStopwatch() {
stopwatch.restart();
}
void drawTextByChar(const String& text, Vec2 pos, Color color, int32 speedInMilliseconds) {
const int32 count = (stopwatch.ms() / speedInMilliseconds);
draw(text.substr(0, count), pos, color);
}
// キャラクター名を表示するメソッド
void drawCharacterName() {
if (showCharacterName) {
draw(characterName, Vec2(120, 650), Palette::White);
}
}
public:
CharacterStateMachine(Font font) : StateMachine(font), previousState(static_cast<CharacterState>(0)) {
setupStates();
}
void addStateWithSpeed(CharacterState state, const String& text, int32 speed, bool showName) {
stateTextSpeeds[state] = speed;
addState(state, [=, this]() mutable {
if (previousState != state) {
currentStateText = text;
showCharacterName = showName;
resetStopwatch();
previousState = state;
}
drawTextByChar(currentStateText, Vec2(200, 700), Palette::White, stateTextSpeeds[state]);
});
}
void update() {
drawCharacterName();
StateMachine::update();
}
void setupStates() {
// ステートの設定
addStateWithSpeed(CharacterState::CallingForHelp1, U"こちらです、助けてください…!", 30, true);
addStateWithSpeed(CharacterState::CallingForHelp2, U"お願い…私を放っておかないで…", 100, false);
addStateWithSpeed(CharacterState::CallingForHelp3, U"誰か!助けてください!", 60, false);
addStateWithSpeed(CharacterState::DamageByPlayer, U"気にしないで、大丈夫ですから…", 30, true);
addStateWithSpeed(CharacterState::DamageByEnemy, U"まだ平気です、助けて…", 30, true);
addStateWithSpeed(CharacterState::PlayerDamaged1, U"大丈夫ですか?私、心配です…", 30, true);
addStateWithSpeed(CharacterState::PlayerDamaged2, U"本当にありがとう…", 30, true);
addStateWithSpeed(CharacterState::PlayerDamaged3, U"逃げてください!!それ以上はもう…!!", 30, true);
addStateWithSpeed(CharacterState::DeathByCollisionNarration, U"不運にもロボの機体が彼女の家を破壊した。", 20, false);
addStateWithSpeed(CharacterState::DeathByCollision, U"こんな終わり方、望んでなかった…", 70, true);
addStateWithSpeed(CharacterState::DeathByShotNarration, U"ロボの銃弾が彼女を撃ち抜いた。", 20, false);
addStateWithSpeed(CharacterState::DeathByShot, U"あなたのこと、ずっと覚えています…", 90, true);
addStateWithSpeed(CharacterState::DeathByEnemyNarration, U"敵の攻撃がとどめをさした。最後の言葉がこだまする。", 20, false);
addStateWithSpeed(CharacterState::DeathByEnemy, U"ごめんなさい、皆さん…", 140, true);
addStateWithSpeed(CharacterState::PlayerDeathNarration, U"惜しくも、ロボの体力は限界まで到達した。", 20, false);
addStateWithSpeed(CharacterState::PlayerDeath, U"こんなに早く…さよならなんて言いたくなかったです…", 90, true);
addStateWithSpeed(CharacterState::NearRescue, U"もう少し…持ちこたえて…", 30, true);
addStateWithSpeed(CharacterState::RescuedNarration, U"避難までの時間を稼ぐことで、彼女を救うことできた。", 20, false);
addStateWithSpeed(CharacterState::Rescued, U"助けてくれてありがとう。本当に感謝しています。", 80, true);
}
};
#endif // CHARACTERSTATEMACHINE_HPP
#ifndef CHARACTERSTATES_HPP
#define CHARACTERSTATES_HPP
enum class CharacterState {
CallingForHelp1,
CallingForHelp2,
CallingForHelp3,
DamageByPlayer,
DamageByEnemy,
PlayerDamaged1,
PlayerDamaged2,
PlayerDamaged3,
DeathByCollisionNarration,
DeathByCollision,
DeathByShotNarration,
DeathByShot,
DeathByEnemyNarration,
DeathByEnemy,
PlayerDeathNarration,
PlayerDeath,
NearRescue,
RescuedNarration,
Rescued
};
#endif // CHARACTERSTATES_HPP
# include <Siv3D.hpp>
# include "CharacterStateMachine.cpp"
// ゲームの状態を表す enum
enum class GameState {
Title,
CharacterSelection,
Playing,
BadEndCollision, // プレイヤーがぶつかって死亡
BadEndShot, // プレイヤーが撃ち抜いて死亡
BadEndDefeat, // 敵に倒されて死亡
GoodEnd // 救う
};
// エンディングの状態を
enum class EndingState {
BadEndCollision, // プレイヤーがぶつかって死亡
BadEndShot, // プレイヤーが撃ち抜いて死亡
BadEndDefeat, // 敵に倒されて死亡
GoodEnd // 救う
};
// 敵の位置をランダムに作成する関数
Vec2 GenerateEnemy()
{
return RandomVec2({ 240, 1040 }, -20);
}
void Main()
{
// ゲームの初期状態
GameState gameState = GameState::Title;
// エンディングの初期状態
EndingState endingState = EndingState::GoodEnd;
Scene::SetBackground(ColorF{ 0, 0, 0});
// ウィンドウを 1280x900 にリサイズする
Window::Resize(1280, 900);
// フォント
const Font font{ FontMethod::MSDF, 36 };
// テクスチャ
const Texture playerTexture{ U"🤖"_emoji };
const Texture enemyTexture{ U"👾"_emoji };
const Texture houseTexture{ U"🏠"_emoji };
// 画像ファイルから Image を作成
const Image image{ U"DALL·E 2023-11-04 23.35.47 - An anime-style illustration of a woman named Hikari in a grateful pose. She is clad in futuristic survival gear, sporting a vibrant blue and white jac.png" };
// Image から Texture を作成
const Texture texture{ image };
// HP
constexpr int InitialPlayerHP = 50;
int playerHP = InitialPlayerHP;
constexpr int InitialHouseHP = 10;
int houseHP = InitialHouseHP;
// ポジション
Vec2 playerPos{ 640, 500 };
Array<Vec2> enemies = { GenerateEnemy() };
Vec2 housePos{ 640, 300 };
// ショット
Array<Vec2> playerBullets;
Array<Vec2> enemyBullets;
// スピード
constexpr double PlayerSpeed = 550.0;
constexpr double PlayerBulletSpeed = 500.0;
constexpr double EnemySpeed = 100.0;
constexpr double EnemyBulletSpeed = 300.0;
// 敵の発生間隔の初期値(秒)
constexpr double InitialEnemySpawnInterval = 2.0;
// 敵の発生間隔(秒)
double enemySpawnTime = InitialEnemySpawnInterval;
// 敵の発生の蓄積時間(秒)
double enemyAccumulatedTime = 0.0;
// 敵ショットのクールタイム(秒)
constexpr double EnemyShotCoolTime = 0.9;
// 敵ショットのクールタイムタイマー(秒)
double enemyShotTimer = 0.0;
// 避難までの時間(秒)
constexpr double InitialEscapeTime = 30;
double escapeTime = InitialEscapeTime;
// ゲームプレイの初期化
bool InitializedPlaying = false;
Effect effect;
// ステートマシン
CharacterStateMachine characterSM(font);
characterSM.setupStates();
double timeSinceStateChange = 0;
while (System::Update())
{
// マウスカーソルを非表示に
Cursor::RequestStyle(CursorStyle::Hidden);
const double deltaTime = Scene::DeltaTime();
// ステートマシンの更新
characterSM.update();
switch (gameState)
{
// タイトル画面の処理
case GameState::Title:
timeSinceStateChange += deltaTime;
characterSM.setState(CharacterState::CallingForHelp3);
if(timeSinceStateChange >= 4){
characterSM.setState(CharacterState::CallingForHelp2);
}
// 'Enter' キーでキャラクター選択へ
if (KeyEnter.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::CharacterSelection;
}
break;
// キャラクター選択画面の処理
case GameState::CharacterSelection:
characterSM.setState(CharacterState::CallingForHelp1);
// 選択が完了したら 'H' キーでゲーム開始
if (KeyH.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::Playing;
}
break;
// バッドエンド
case GameState::BadEndCollision:
timeSinceStateChange += deltaTime;
characterSM.setState(CharacterState::DeathByCollisionNarration);
if(timeSinceStateChange >= 2){
characterSM.setState(CharacterState::DeathByCollision);
}
// 'R' キーでタイトル画面へ
if (KeyR.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::Title;
}
break;
case GameState::BadEndShot:
timeSinceStateChange += deltaTime;
characterSM.setState(CharacterState::DeathByShotNarration);
if(timeSinceStateChange >= 2){
characterSM.setState(CharacterState::DeathByShot);
}
// 'R' キーでタイトル画面へ
if (KeyR.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::Title;
}
break;
case GameState::BadEndDefeat:
timeSinceStateChange += deltaTime;
characterSM.setState(CharacterState::DeathByEnemyNarration);
if(timeSinceStateChange >= 2){
characterSM.setState(CharacterState::DeathByEnemy);
}
// 'R' キーでタイトル画面へ
if (KeyR.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::Title;
}
break;
// グッドエンド
case GameState::GoodEnd:
timeSinceStateChange += deltaTime;
characterSM.setState(CharacterState::RescuedNarration);
if(timeSinceStateChange >= 3){
characterSM.setState(CharacterState::Rescued);
}
// 'R' キーでタイトル画面へ
if (KeyR.pressed()) {
timeSinceStateChange = 0;
gameState = GameState::Title;
}
break;
// ゲームプレイ中の処理
case GameState::Playing:
// ゲームエンド判定
bool gameend = false;
escapeTime -= deltaTime;
enemyAccumulatedTime += deltaTime;
enemyShotTimer += deltaTime;
// 敵を発生させる
while (enemySpawnTime <= enemyAccumulatedTime)
{
enemyAccumulatedTime -= enemySpawnTime;
enemySpawnTime = Max(enemySpawnTime * 0.95, 0.3);
enemies << GenerateEnemy();
}
// 自機の移動
const Vec2 move = Vec2{ (KeyD.pressed() - KeyA.pressed()), (KeyS.pressed() - KeyW.pressed()) }
.setLength(deltaTime * PlayerSpeed * (KeyShift.pressed() ? 0.5 : 1.0));
playerPos.moveBy(move).clamp(Scene::Rect());
// 自機ショットの発射
if (KeyF.down())
{
playerBullets << playerPos.movedBy(0, -50);
}
// 自機ショットを移動させる
for (auto& playerBullet : playerBullets)
{
playerBullet.y += (deltaTime * -PlayerBulletSpeed);
}
// 画面外に出た自機ショットを削除する
playerBullets.remove_if([](const Vec2& b) { return (b.y < -40); });
// 敵を移動させる
// 画面の中央座標を指定
float screenCenterX = 640.0f; // 画面の中央のX座標
float screenCenterY = 300.0f; // 画面の中央のY座標
// 敵を画面の中央に向かって移動させる
for (auto& enemy : enemies)
{
// 画面の中央に向かうベクトルを計算
float dirX = screenCenterX - enemy.x;
float dirY = screenCenterY - enemy.y;
// ベクトルの長さを計算(距離)
float length = sqrt(dirX * dirX + dirY * dirY);
// 長さが0でないならば、ベクトルを正規化(方向のみを保持)
if (length > 0.01f) // ゼロ除算を避けるための微小値
{
dirX /= length;
dirY /= length;
}
// 敵を移動させる(正規化された方向ベクトルにスピードと時間を乗算)
enemy.x += dirX * (deltaTime * EnemySpeed);
enemy.y += dirY * (deltaTime * EnemySpeed);
// オプション: 画面中央に到達したら、さらに移動しないようにする
if (sqrt(pow(enemy.x - screenCenterX, 2) + pow(enemy.y - screenCenterY, 2)) < (deltaTime * EnemySpeed)) {
enemy.x = screenCenterX;
enemy.y = screenCenterY;
}
}
////////////////////////////////
//
// 攻撃判定
//
////////////////////////////////
// 敵 vs 自機ショット
for (auto itEnemy = enemies.begin(); itEnemy != enemies.end();)
{
const Circle enemyCircle{ *itEnemy, 40 };
bool skip = false;
for (auto itBullet = playerBullets.begin(); itBullet != playerBullets.end();)
{
if (enemyCircle.intersects(*itBullet))
{
// 爆発エフェクトを追加する
effect.add([pos = *itEnemy](double t)
{
const double t2 = ((0.5 - t) * 2.0);
Circle{ pos, (10 + t * 280) }.drawFrame((20 * t2), AlphaF(t2 * 0.5));
return (t < 0.5);
});
itEnemy = enemies.erase(itEnemy);
playerBullets.erase(itBullet);
skip = true;
break;
}
++itBullet;
}
if (skip)
{
continue;
}
++itEnemy;
}
// 敵 vs 自機の衝突判定
for (auto itEnemy = enemies.begin(); itEnemy != enemies.end();)
{
const Circle enemyCircle{ *itEnemy, 60 };
if (enemyCircle.intersects(playerPos))
{
playerHP -= 5;
if(playerHP >= 40){
characterSM.setState(CharacterState::PlayerDamaged1);
}
else if (playerHP >= 20){
characterSM.setState(CharacterState::PlayerDamaged2);
}
else{
characterSM.setState(CharacterState::PlayerDamaged3);
}
// 爆発エフェクトを追加する
effect.add([pos = *itEnemy](double t)
{
const double t2 = ((0.5 - t) * 2.0);
Circle{ pos, (10 + t * 280) }.drawFrame((20 * t2), AlphaF(t2 * 0.5));
return (t < 0.5);
});
itEnemy = enemies.erase(itEnemy);
}
else
{
++itEnemy;
}
}
// 敵 vs 家の衝突判定
for (auto itEnemy = enemies.begin(); itEnemy != enemies.end();)
{
const Circle enemyCircle{ *itEnemy, 40 };
if (enemyCircle.intersects(housePos))
{
itEnemy = enemies.erase(itEnemy);
houseHP -= 5;
if(houseHP <= 0){
gameend = true;
endingState = EndingState::BadEndDefeat;
}
else{
characterSM.setState(CharacterState::DamageByEnemy);
// 爆発エフェクトを追加する
effect.add([pos = *itEnemy](double t)
{
const double t2 = ((0.5 - t) * 2.0);
Circle{ pos, (10 + t * 280) }.drawFrame((20 * t2), AlphaF(t2 * 0.5));
return (t < 0.5);
});
}
}
else
{
++itEnemy;
}
}
// プレイヤーのショット vs 家の衝突判定
for (auto itBullet = playerBullets.begin(); itBullet != playerBullets.end();)
{
const Circle bulletCircle{ *itBullet, 20 }; // 仮の弾のサイズ
if (bulletCircle.intersects(housePos))
{
itBullet = playerBullets.erase(itBullet);
houseHP -= 5;
if(houseHP <= 0){
gameend = true;
endingState = EndingState::BadEndShot;
}
else{
characterSM.setState(CharacterState::DamageByPlayer);
// 爆発エフェクトを追加する
effect.add([pos = *itBullet](double t)
{
const double t2 = ((0.5 - t) * 2.0);
Circle{ pos, (10 + t * 280) }.drawFrame((20 * t2), AlphaF(t2 * 0.5));
return (t < 0.5);
});
}
}
else
{
++itBullet;
}
}
// 自機 vs 家の衝突判定
{
const Circle playerCircle{ playerPos, 30 }; // 仮の自機のサイズ
if (playerCircle.intersects(housePos))
{
houseHP -= 10;
endingState = EndingState::BadEndCollision;
gameend = true;
}
}
if(escapeTime <= 0){
gameend = true;
endingState = EndingState::GoodEnd;
}
if(escapeTime <= 3){
characterSM.setState(CharacterState::NearRescue);
}
// ゲームエンドならリセットする
if (gameend)
{
playerPos = Vec2{ 640, 500 };
enemies.clear();
playerBullets.clear();
enemyBullets.clear();
enemySpawnTime = InitialEnemySpawnInterval;
houseHP = InitialHouseHP;
playerHP = InitialPlayerHP;
escapeTime = InitialEscapeTime;
switch (endingState) {
case EndingState::BadEndCollision:
gameState = GameState::BadEndCollision;
break;
case EndingState::BadEndShot:
gameState = GameState::BadEndShot;
break;
case EndingState::BadEndDefeat:
gameState = GameState::BadEndDefeat;
break;
case EndingState::GoodEnd:
gameState = GameState::GoodEnd;
break;
default:
gameState = GameState::Title;
break;
}
}
break;
}
////////////////////////////////
//
// 描画
//
////////////////////////////////
switch (gameState)
{
case GameState::Title:
font(U"君を救うロボット").draw(80, Vec2{ 600, 300 }, Palette::White);
font(U"[Enter] ゲームスタート").draw(30, Vec2{ 600, 500 }, Palette::White);
font(U"[esc] ゲーム終了").draw(30, Vec2{ 600, 540 }, Palette::White);
break;
case GameState::CharacterSelection:
font(U"救う人").draw(80, Vec2{ 600, 300 }, Palette::White);
font(U"[H] ヒカリ (ロボットの家族)").draw(30, Vec2{ 600, 500 }, Palette::White);
// font(U"[?] FutureWork").draw(30, Vec2{ 600, 540 }, Palette::White);
break;
case GameState::BadEndCollision:
case GameState::BadEndShot:
case GameState::BadEndDefeat:
font(U"[R] 時をもどす").draw(30, Vec2{ 1000, 500 }, Palette::White);
break;
case GameState::GoodEnd:
if(timeSinceStateChange >= 7.5){
texture.draw();
font(U"fin").draw(60, Vec2{ 1100, 400 }, Palette::White);
}
if(timeSinceStateChange >= 12){
font(U"[esc] ゲーム終了").draw(20, Vec2{ 1050, 820 }, Palette::White);
font(U"[R] 時をもどす").draw(20, Vec2{ 1050, 850 }, Palette::White);
}
break;
case GameState::Playing:
// 背景のアニメーションを描画する
for (int32 i = 0; i < 12; ++i)
{
const double a = Periodic::Sine0_1(2s, Scene::Time() - (2.0 / 12 * i));
Rect{ 240, (i * 50), 800, 50 }.draw(ColorF(1.0, a * 0.2));
}
// 自機を描画する
playerTexture.resized(80).flipped().drawAt(playerPos);
// 自機ショットを描画する
for (const auto& playerBullet : playerBullets)
{
Circle{ playerBullet, 8 }.draw(Palette::Orange);
}
// 敵を描画する
for (const auto& enemy : enemies)
{
enemyTexture.resized(60).drawAt(enemy);
}
// 家を描画する
houseTexture.resized(50).drawAt(housePos);
// 爆発エフェクトを描画する
effect.update();
// スコアを描画する
font(U"プレイヤーのHP").draw(24, Arg::bottomLeft(20, 50));
font(U"{}"_fmt(playerHP)).draw(24, Arg::bottomLeft(20, 80));
font(U"家のHP").draw(24, Arg::bottomLeft(20, 110));
font(U"{}"_fmt(houseHP)).draw(24, Arg::bottomLeft(20, 140));
font(U"避難までの時間").draw(24, Arg::bottomLeft(20, 170));
font(U"{:.2f}"_fmt(escapeTime)).draw(24, Arg::bottomLeft(20, 200));
// 操作方法を描画
font(U"[WASD] 移動").draw(24, Arg::bottomRight(1240, 50));
font(U"[F] 発射").draw(24, Arg::bottomRight(1240, 80));
break;
}
}
}
#ifndef STATEMACHINE_HPP
#define STATEMACHINE_HPP
#include <Siv3D.hpp>
#include <functional>
#include <unordered_map>
template<typename T>
class StateMachine {
private:
T currentState;
Font font;
std::unordered_map<T, std::function<void()>> stateFunctions;
public:
StateMachine(Font font) : font(font) {}
// ステートを追加するメソッド
void addState(T state, std::function<void()> function) {
stateFunctions[state] = function;
}
// ステートをセットするメソッド
void setState(T newState) {
currentState = newState;
}
// 現在のステートの関数を実行する
void update() {
if (stateFunctions.find(currentState) != stateFunctions.end()) {
stateFunctions[currentState]();
}
}
// テキストを描画する汎用メソッド
void draw(const String& text, Vec2 position, ColorF color) const {
font(text).draw(Arg::leftCenter(position), color);
}
};
#endif // STATEMACHINE_HPP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment