はじめに
- 前々回に作成したスネークゲームの書き直しです.
- 前回の作り方だと以下の課題点がありました.
- エサの重複表示
- スネークとエサの重複表示
- エサの出現パターンの不安定性
- 時間管理の面倒さ
- フレームが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 | 編集後記 |
デモ
開発環境
- 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次元配列で管理できるとすっきりする.
- 後は、ゲームが下手だとデバッグがあんまりできないから困る.
