1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++とSDL3でスネークゲーム作りに再挑戦

1
Posted at

はじめに

  • 前々回に作成したスネークゲームの書き直しです.
  • 前回の作り方だと以下の課題点がありました.
  1. エサの重複表示
  2. スネークとエサの重複表示
  3. エサの出現パターンの不安定性
  4. 時間管理の面倒さ
  5. フレームが60FPSではなかった
  • 今回はこの問題を解決しながら、前回と同じ仕様にするために2次元配列を用いて画面を管理しました.
  • 前々回の記事も残しておくので、違いなどを見比べるのもいいかもしれません.
  • 独学・趣味でプログラムを書いています.
  • そのため、中身はあまり参考になりません.
  • 自作のコードの置き場として使ってます.

目次

1 2
1 はじめに
2 デモ
3 開発環境
4 仕様
5 コード
5-1 Main.cpp
5-2 Config.cpp
5-3 Snake.cpp
5-4 Food.cpp
6 編集後記

デモ

SnakeGame_Sample02.gif

開発環境

  • SDL3のバージョンが変わってますが、3.4.0でも変わらないと思います.
  • OS : Windows11
  • IDE : Visual Studio Community 2026
  • バージョン : SDL3.4.2

仕様

  • 仕様は前々回と変えていません.
  • スペースキー = ゲームスタート/ゲームオーバー後のリスタート
  • 各矢印キー = スネークの操作
  • 壁またはスネーク自身の体に頭がぶつかるとゲームオーバー
  • エサを取ると体が1つ伸びる

コード

Main.cpp

Main.cpp
#define SDL_MAIN_USE_CALLBACKS 1

// ----------  ライブラリ  ----------
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#include <vector>

#include "Config.h"
#include "Snake.h"
#include "Food.h"

// ----------  状態管理用の構造体  ----------
struct AppState
{
	// ウィンドウ
	SDL_Window   *window   = nullptr;
	// レンダラー
	SDL_Renderer *renderer = nullptr;
	// ゲームのスタート管理
	bool          isGameStart = false;
	// ゲームボード
	char board[WINDOW_HEIGHT][WINDOW_WIDTH] = { BLANK };
	// ゲームボードのBLANKの位置を保存するリスト
	std::vector<Point> emptyCells;
	// スネーク
	Snake snake;
	// エサ
	std::vector<Food> foods;
	// 出現させるエサの数
	int numOfFood = 8;
};

// ----------  初期化  ----------
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
	// 状態管理
	AppState *as = new AppState();
	*appstate = as;

	// メタ情報
	SDL_SetAppMetadata("Snake Game", "0.1", "com.example.snake_game");

	// SDLの初期化
	if( !SDL_Init(SDL_INIT_VIDEO) )
	{
		SDL_Log("SDL Init Error: %s\n", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// ウィンドウ/レンダラーの作成
	if( !SDL_CreateWindowAndRenderer("Snake Game", WINDOW_WIDTH * (int)PIXEL_SIZE, WINDOW_HEIGHT * (int)PIXEL_SIZE, SDL_WINDOW_RESIZABLE, &as->window, &as->renderer) )
	{
		SDL_Log("SDL Create Window and Renderer Error: %s\n", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 垂直同期の設定
	SDL_SetWindowSurfaceVSync(as->window, 1);
	SDL_SetRenderVSync(as->renderer, 1);

	return SDL_APP_CONTINUE;
}

// ----------  イベント処理  ----------
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
	// 状態管理
	AppState *as = (AppState *)appstate;

	// 閉じるボタンを押したとき(終了)
	if( event->type == SDL_EVENT_QUIT )
	{
		return SDL_APP_SUCCESS;
	}
	// スペースキーを押したとき(ゲームスタート/リスタート)
	else if( !as->isGameStart && event->key.key == SDLK_SPACE && !event->key.down )
	{
		// ゲームスタート
		as->isGameStart = true;

		// ゲームボードのBLANKの位置を保存するリストをリセット
		as->emptyCells.clear();
		// 初期のスネークの位置以外のBLANKの位置を保存
		for( char y = 0; y < WINDOW_HEIGHT; y++ )
		{
			for( char x = 0; x < WINDOW_WIDTH; x++ )
			{
				if( y == SNAKE_INIT_POS_Y && x == SNAKE_INIT_POS_X[0] || x == SNAKE_INIT_POS_X[1] || x == SNAKE_INIT_POS_X[2] )
				{
					continue;
				}
				as->emptyCells.push_back({ x, y });
			}
		}

		// スネークの初期化
		as->snake.init();

		// エサの出現数の初期化
		as->numOfFood = 8;

		// エサの初期化
		as->foods.clear();
		for( int i = 0; i < as->numOfFood; i++ )
		{
			Food food;
			food.init(as->board, as->emptyCells);
			as->foods.push_back(food);
		}
	}
	// 上矢印キーを押したとき
	else if( as->snake.getDirection() != DIRECT_UP && as->snake.getDirection() != DIRECT_DOWN && as->isGameStart && event->key.key == SDLK_UP && !event->key.down )
	{
		as->snake.setDirection(DIRECT_UP);
	}
	// 右矢印キーを押したとき
	else if( as->snake.getDirection() != DIRECT_RIGHT && as->snake.getDirection() != DIRECT_LEFT && as->isGameStart && event->key.key == SDLK_RIGHT && !event->key.down )
	{
		as->snake.setDirection(DIRECT_RIGHT);
	}
	// 下矢印キーを押したとき
	else if( as->snake.getDirection() != DIRECT_UP && as->snake.getDirection() != DIRECT_DOWN && as->isGameStart && event->key.key == SDLK_DOWN && !event->key.down )
	{
		as->snake.setDirection(DIRECT_DOWN);
	}
	// 左矢印キーを押したとき
	else if( as->snake.getDirection() != 0 && as->snake.getDirection() != DIRECT_RIGHT && as->snake.getDirection() != DIRECT_LEFT && as->isGameStart && event->key.key == SDLK_LEFT && !event->key.down )
	{
		as->snake.setDirection(DIRECT_LEFT);
	}

	return SDL_APP_CONTINUE;
}

// ----------  ゲームループ  ----------
SDL_AppResult SDL_AppIterate(void *appstate)
{
	// 状態管理
	AppState *as = (AppState *)appstate;

	// 背景色の設定(黒色)
	SDL_SetRenderDrawColor(as->renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
	// バックバッファへの書き込み
	SDL_RenderClear(as->renderer);

	// ゲームスタート
	if( as->isGameStart )
	{
		// スネークの情報更新/壁との当たり判定
		as->isGameStart = as->snake.update();

		// ボードの初期化
		if( as->isGameStart )
		{
			for( int y = 0; y < WINDOW_HEIGHT; y++ )
			{
				for( int x = 0; x < WINDOW_WIDTH; x++ )
				{
					as->board[y][x] = BLANK;
				}
			}
		}

		if( as->isGameStart )
		{
			// ボードにスネークの位置を書き込み/頭と体の当たり判定
			as->isGameStart = writeBoardSnake(as->snake, as->board);
		}

		if( as->isGameStart )
		{
			// ボードにエサの位置を書き込み/エサの当たり判定/エサの位置の再生成
			for( Food &food : as->foods )
			{
				food.update(as->board, as->emptyCells, as->numOfFood, as->snake);
				as->board[food.getY()][food.getX()] = FOOD;
			}

			// ボード上でBLANKの位置を取得する
			setEmptyCells(as->board, as->emptyCells);

			// エサをバックバッファへ書き込む
			for( int i = 0; i < as->numOfFood; i++ )
			{
				as->foods[i].draw(as->renderer);
			}

			// スネークをバックバッファへ書き込む
			as->snake.draw(as->renderer);
		}
	}

	// 画面へ書き込み
	SDL_RenderPresent(as->renderer);

	return SDL_APP_CONTINUE;
}

// ----------  終了時処理  ----------
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
	if( appstate )
	{
		AppState *as = (AppState *)appstate;
		SDL_DestroyRenderer(as->renderer);
		SDL_DestroyWindow(as->window);
		delete(as);
	}
}

Config.cpp

Config.h
#pragma once // CONFIG_H

// ----------  ライブラリ  ----------
#include <vector>

// ----------  定数  ----------

// ウィンドウのサイズ
constexpr int WINDOW_WIDTH  = 30;
constexpr int WINDOW_HEIGHT = 20;

// スネーク/エサのサイズ
constexpr float PIXEL_SIZE  = 24.0f;

// キーによる進行方向
constexpr char DIRECT_UP    = 1;
constexpr char DIRECT_RIGHT = 2;
constexpr char DIRECT_DOWN  = 3;
constexpr char DIRECT_LEFT  = 4;

// ボードのマスの意味
constexpr char BLANK = 0;
constexpr char SNAKE = 1;
constexpr char FOOD  = 2;

// スネークの初期状態の長さ
constexpr int SNAKE_INIT_LENGTH = 3;

// スネークの初期位置
constexpr char SNAKE_INIT_POS_X[SNAKE_INIT_LENGTH] = { 15, 14, 13 };
constexpr char SNAKE_INIT_POS_Y    = 9;

// クラスの宣言
class Snake;
class Food;

// 表示位置用の構造体
struct Point
{
	char x;
	char y;
};

// ----------  ヘルパー関数  ----------

// ボードにスネークの位置の書き込み/頭と体の当たり判定
bool writeBoardSnake(Snake &snake, char board[WINDOW_HEIGHT][WINDOW_WIDTH]);

// ボード上でBLANKの位置を取得する関数
void setEmptyCells(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells);
Config.cpp
// ----------  ライブラリ  ----------
#include <SDL3/SDL.h>

#include "Config.h"
#include "Snake.h"
#include "Food.h"

// ----------  ヘルパー関数  ----------

// ボードにスネークの位置の書き込み/頭と体の当たり判定
bool writeBoardSnake(Snake &snake, char board[WINDOW_HEIGHT][WINDOW_WIDTH])
{
	for( Point &body : snake.getBody() )
	{
		// スネークの頭と体の当たり判定
		if( board[body.y][body.x] == SNAKE )
		{
			// スネークの無効化
			snake.setIsNonActive();
			// ゲームオーバー
			return false;
		}
		// スネークの位置の書き込み
		board[body.y][body.x] = SNAKE;
	}

	return true;
}

// ボード上でBLANKの位置を取得する関数
void setEmptyCells(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells)
{
	// BLANKの場所のリストをリセット
	emptyCells.clear();

	// ボード上のBLANKの位置を取得
	for( char y = 0; y < WINDOW_HEIGHT; y++ )
	{
		for( char x = 0; x < WINDOW_WIDTH; x++ )
		{
			if( board[y][x] == BLANK )
			{
				emptyCells.push_back({ x, y });
			}
		}
	}
}

Snake.cpp

Snake.h
#pragma once // SNAKE_H

// ----------  ライブラリ  ----------
#include <SDL3/SDL.h>

#include <vector>

#include "Config.h"

// ----------  クラス  ----------
class Snake
{
public:
	// 初期化関数
	void init();
	// 情報更新用の関数
	bool update();
	// 表示用の関数
	void draw(SDL_Renderer *renderer);
	// スネークを無効化する関数
	void setIsNonActive();
	// 進行方向の取得用の関数
	void setDirection(char direct);
	// エサを食べたことを設定する関数
	void setIsAteFood();
	// 進行方向を取得する関数
	char getDirection();
	// スネークの体のリストを返す関数
	std::vector<Point> &getBody();

private:
	// スネークの移動フレームをカウント
	unsigned int frameCount;
	// 進行方向
	char direction;
	// スネークの有効化
	bool isActive;
	// アイテムの取得のフラグ
	bool isAteFood;
	// 体の座標を保存するVector
	std::vector<Point> bodys;
};
Snake.cpp
// ----------  ライブラリ  ----------
#include "Snake.h"

// ----------  定数  ----------
constexpr int FRAME = 10;

// ----------  メソッド  ----------

// 初期化関数
void Snake::init()
{
	// フレームカウントをリセット
	frameCount = 0;

	// 進行方向の初期化
	direction = 0;

	// 生成可能の合図
	isActive = true;

	// アイテムの取得のフラグの初期化
	isAteFood = false;

	// Vectorの初期化(スネークの体の情報)
	bodys.clear();

	// スネークの表示位置/サイズの初期化
	for( char i = 0; i < SNAKE_INIT_LENGTH; i++ )
	{
		Point point = {
			.x = SNAKE_INIT_POS_X[i],
			.y = SNAKE_INIT_POS_Y
		};
		bodys.push_back(point);
	}
}

// 情報更新用の関数
bool Snake::update()
{
	if( direction == 0 || !isActive ) { return true; }

	// 10フレームごとに更新
	if( frameCount % FRAME == 0 )
	{
		// スネークの頭
		Point head = bodys[0];

		// 進行方向に進める処理
		switch( direction )
		{
			// 上方向
			case DIRECT_UP:
				head.y -= 1;
				break;
				// 右方向
			case DIRECT_RIGHT:
				head.x += 1;
				break;
				// 下方向
			case DIRECT_DOWN:
				head.y += 1;
				break;
				// 左方向
			case DIRECT_LEFT:
				head.x -= 1;
				break;
		}

		// スネークと壁の当たり判定
		if( head.x < 0 || head.x >= WINDOW_WIDTH || head.y < 0 || head.y >= WINDOW_HEIGHT )
		{
			// スネークの無効化
			isActive = false;
			return false;
		}

		// スネークの頭を追加
		bodys.insert(bodys.begin(), head);

		// エサを食べていないとき
		if( !isAteFood )
		{
			bodys.pop_back();
		}

		// エサの取得をリセット
		isAteFood = false;
	}

	// フレームのカウント
	frameCount++;

	return true;
}

// 表示用の関数
void Snake::draw(SDL_Renderer *renderer)
{
	// スネークが有効化されていなければ終了
	if( !isActive ) { return; }

	// スネークの頭のみ赤色を設定するためのフラグ
	bool isHead = true;

	// スネークの体の長さ分だけ表示
	for( Point &body : bodys )
	{
		// スネークの色設定(頭 : 赤色)
		if( isHead )
		{
			SDL_SetRenderDrawColor(renderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
			isHead = false;
		}
		// スネークの色設定(体 : 緑色)
		else
		{
			SDL_SetRenderDrawColor(renderer, 0, 255, 0, SDL_ALPHA_OPAQUE);
		}
		// スネークの表示位置/サイズの設定
		SDL_FRect rect = {
		.x = body.x * PIXEL_SIZE,
		.y = body.y * PIXEL_SIZE,
		.w = PIXEL_SIZE,
		.h = PIXEL_SIZE
		};
		// バックバッファへ書き込み
		SDL_RenderFillRect(renderer, &rect);
	}
}

// スネークを無効化する関数
void Snake::setIsNonActive()
{
	isActive = false;
}

// 進行方向の取得用の関数
void Snake::setDirection(char direct)
{
	direction = direct;
}

// エサを食べたことを設定する関数
void Snake::setIsAteFood()
{
	isAteFood = true;
}

// 進行方向を取得する関数
char Snake::getDirection()
{
	return direction;
}

// スネークの体のリストを返す関数
std::vector<Point> &Snake::getBody()
{
	return bodys;
}

Food.cpp

Food.h
#pragma once // FOOD_H

// ----------  ライブラリ  ----------
#include <SDL3/SDL.h>

#include "Config.h"

// ----------  クラス  ----------
class Food
{
public:
	// 初期化関数
	void init(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells);
	// 情報更新用の関数
	void update(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells, int numOfFood, Snake &snake);
	// 表示用の関数
	void draw(SDL_Renderer *renderer);
	// エサの位置を返す関数
	char getX();
	char getY();

private:
	// エサの表示位置
	Point point;
	// アイテムの有効化
	bool isActive;
};
Food.cpp
// ----------  ライブラリ  ----------
#include "Food.h"
#include "Snake.h"

// ----------  メソッド  ----------

// 初期化関数
void Food::init(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells)
{
	// 初期位置のスネーク以外の場所にエサをランダムに生成
	int index = SDL_rand(emptyCells.size());
	point = emptyCells[index];
	emptyCells.erase(emptyCells.begin() + index);

	// 生成可能の合図
	isActive = true;
}

// エサの当たり判定/エサの位置の再生成用の関数
void Food::update(char board[WINDOW_HEIGHT][WINDOW_WIDTH], std::vector<Point> &emptyCells, int numOfFood, Snake &snake)
{
	if( !isActive ) { return; }

	// エサの当たり判定
	if( board[point.y][point.x] == SNAKE && !emptyCells.empty() )
	{
		// エサの出現可能があるとき
		if( emptyCells.size() >= numOfFood )
		{
			// エサの位置を再生成
			int index = SDL_rand(emptyCells.size());
			point = emptyCells[index];
		}
		else
		{
			isActive = false;
		}
		// スネークの体が1つ伸びる
		snake.setIsAteFood();
	}
}

// 表示用の関数
void Food::draw(SDL_Renderer *renderer)
{
	if( !isActive ) { return; }

	// エサの表示位置/サイズ
	SDL_FRect dstRect = {
		.x = point.x * PIXEL_SIZE,
		.y = point.y * PIXEL_SIZE,
		.w = PIXEL_SIZE,
		.h = PIXEL_SIZE
	};

	// エサの色の設定
	SDL_SetRenderDrawColor(renderer, 255, 255, 0, SDL_ALPHA_OPAQUE);
	// バックバッファへの書き込み
	SDL_RenderFillRect(renderer, &dstRect);
}

// エサのx値を返す関数
char Food::getX()
{
	return point.x;
}

// エサのx値を返す関数
char Food::getY()
{
	return point.y;
}

編集後記

  • 前々回よりもめっちゃこんがらがった.
  • でも画面を2次元配列で管理できるとすっきりする.
  • 後は、ゲームが下手だとデバッグがあんまりできないから困る.
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?