0
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でシューティングゲームを作ってみた

0
Last updated at Posted at 2026-02-28

はじめに

  • このコードはまだ実装途中です!

  • 順次書き足していきます.

  • 今回もGeminiの手を借りながらC++とSDL3でゲーム作りをしています.

  • 私は独学・趣味でプログラムを書いています.

  • そのため、中身はあまり参考になりません.

  • 自作のコードの置き場として使ってます.

  • 追記
    3/1 Bulletクラスの追加 / 弾の発射機能
    3/2 Enemyクラスの追加 / ゲーム機能の基礎の完成
    3/3 垂直同期の追加/それによる速度調整/コード修正

目次

1 2
1 はじめに
2 デモ
3 開発環境
4 仕様
5 使用した画像
6 コード
6-1 Main.cpp
6-2 Config.h
6-3 Plane.cpp
6-4 Bullet.cpp
6-5 Enemy.cpp

デモ

ShootingGame_Sample04.gif

開発環境

  • OS : Windows11
  • IDE : Visual Studio Community 2026
  • SDL3.4.2
  • SDL_Image3.4.0

仕様

  • スペースキー = ゲームスタート / ゲームオーバー後のリスタート

  • 矢印キー = 自機の操作

  • Fキー = 弾の発射

  • 弾が敵に当たると敵が消える

  • 自機が敵に当たるとゲームオーバー

使用した画像

  • 自作なのでクオリティはよくないです.
    SpriteSheet.png

コード

Main.cpp

Main.cpp
#define SDL_MAIN_USE_CALLBACKS 1

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

#include <vector>

#include "Config.h"
#include "Plane.h"
#include "Bullet.h"
#include "Enemy.h"

// -----------  定数  -----------
// キーによる進行方向
constexpr char DIRECT_UP     = 1;
constexpr char DIRECT_RIGHT  = 2;
constexpr char DIRECT_DOWN   = 3;
constexpr char DIRECT_LEFT   = 4;
// 初期/追加する敵の数
constexpr int  ENEMY_NUM     = 3;
// 敵の表示寿命の位置
constexpr float DRAW_LIMIT = 32.0f;

// -----------  状態管理用の構造体  -----------
struct AppState
{
	// ウィンドウ
	SDL_Window   *window      = nullptr;
	// レンダラー
	SDL_Renderer *renderer    = nullptr;
	// 画像
	SDL_Texture  *spritesheet = nullptr;
	// ゲームのスタート管理
	bool          isGameStart =   false;
	// 自機
	Plane                plane;
	// 敵
	std::vector<Enemy> enemies;
};

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

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

	if( !SDL_Init(SDL_INIT_VIDEO) )
	{
		SDL_Log("SDL Init is Failed: %s\n", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	if( !SDL_CreateWindowAndRenderer("Shooting Game", WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_RESIZABLE, &as->window, &as->renderer) )
	{
		SDL_Log("SDL Create Window/Renderer is Failed: %s\n", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 画像の読み込み(PNG)
	as->spritesheet = IMG_LoadTexture(as->renderer, "image/SpriteSheet.png");
	if( !as->spritesheet )
	{
		SDL_Log("SDL IMG is Failed: %s\n", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 画像がぼやけないようにする設定
	SDL_SetTextureScaleMode(as->spritesheet, SDL_SCALEMODE_NEAREST);

	// 垂直同期の設定
	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->enemies.clear();
		as->plane.init();
		as->isGameStart = true;
	}
	// Fキーを押したとき(弾の発射)
	else if( as->isGameStart && event->key.key == SDLK_F && !(event->key.down) )
	{
		Bullet newBullet;
		newBullet.init(as->plane.getX(), as->plane.getY());
		as->plane.setBullet(newBullet);
	}
	
	return SDL_APP_CONTINUE;
}

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

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

	// ゲームスタート
	if( as->isGameStart )
	{
		// 敵の出現
		if( (int)as->enemies.size() <= 2 )
		{
			for( unsigned int i = 0; i < ENEMY_NUM; i++ )
			{
				Enemy newEnemy;
				newEnemy.init();
				as->enemies.push_back(newEnemy);
			}
		}

		// 押されているキーの反応取得(飛行機の移動)
		const bool *keysState = SDL_GetKeyboardState(nullptr);
		// 上矢印キー
		if( keysState[SDL_SCANCODE_UP] )
		{
			as->plane.move(DIRECT_UP);
		}
		// 右矢印キー
		if( keysState[SDL_SCANCODE_RIGHT] )
		{
			as->plane.move(DIRECT_RIGHT);
		}
		// 下矢印キー
		if( keysState[SDL_SCANCODE_DOWN] )
		{
			as->plane.move(DIRECT_DOWN);
		}
		// 左矢印キー
		if( keysState[SDL_SCANCODE_LEFT] )
		{
			as->plane.move(DIRECT_LEFT);
		}

		// 敵の情報更新
		for( Enemy &segment : as->enemies )
		{
			segment.update();
		}

		// 自機/弾の情報更新
		as->plane.update();

		// 敵が画面外に出たときの処理
		for( Enemy &segment : as->enemies )
		{
			if( segment.getY() > WINDOW_HEIGHT )
			{
				segment.setIsNonActive();
			}
		}

		// 当たり判定(弾と敵)
		for( Bullet &bullet : as->plane.getBullet() )
		{
			if( !bullet.getIsActive() ) { continue; }

			for( Enemy &enemy : as->enemies )
			{
				if( !enemy.getIsActive() ) { continue; }

				SDL_FRect bRect = bullet.getRect();
				SDL_FRect eRect = enemy.getRect();

				if( SDL_HasRectIntersectionFloat(&bRect, &eRect) )
				{
					bullet.setIsNonActive();
					enemy.setIsNonActive();
				}
			}
		}

		// 当たり判定(敵と自機)
		for( Enemy &enemy : as->enemies )
		{
			SDL_FRect eRect = enemy.getRect();
			SDL_FRect pRect = as->plane.getRect();

			if( SDL_HasRectIntersectionFloat(&eRect, &pRect) )
			{
				enemy.setIsNonActive();
				as->plane.setIsNonActive();
				as->isGameStart = false;
			}
		}

		// 敵の数の掃除
		for( int i = (int)as->enemies.size() - 1; i >= 0; i-- )
		{
			if( !as->enemies[i].getIsActive() )
			{
				as->enemies.erase(as->enemies.begin() + i);
			}
		}

		// 敵の表示
		for( Enemy &segment : as->enemies )
		{
			segment.draw(as->renderer, as->spritesheet);
		}

		// 自機の表示
		as->plane.draw(as->renderer, as->spritesheet);
	}

	// 画面へ書き込み
	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.h

Config.h
#pragma once // CONFIG_H

// -----------  定数  -----------
// ウィンドウのサイズ
constexpr int WINDOW_WIDTH  = 320;
constexpr int WINDOW_HEIGHT = 640;
// 自機/敵のサイズ(縦横)
constexpr float CHARACTER_SIZE = 32.0f;
// 自機のプロペラ/敵のコマ数
constexpr int   FRAME = 3;
// コマ送りをゆっくりにする定数
constexpr int   ANIME_SPEED = 10;

Plane.cpp

Plane.h
#pragma once // PLANE_H

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

#include <vector>

#include "Config.h"
#include "Bullet.h"

// ----------  クラス  ----------
class Plane
{
public:
	// 初期化関数
	void init();
	// コマ送り/弾の管理用の関数
	void update();
	// 進行方向に位置を変更/壁の当たり判定用の関数
	void move(char direct);
	// 表示用の関数
	void draw(SDL_Renderer *renderer, SDL_Texture *spritesheet);
	// 自機の位置を返す関数
	float getX();
	float getY();
	// 自機の無効化用の関数(敵に当たったとき)
	void setIsNonActive();
	// 表示させる弾を保存する関数
	void setBullet(Bullet &bullet);
	// 弾のリストを返す関数
	std::vector<Bullet>& getBullet();
	// 自機の位置/サイズを返す関数
	SDL_FRect getRect();

private:
	// 1フレームごとにカウント
	unsigned int frameCount;
	// 画像の切り出し位置/サイズ
	SDL_FRect srcRect;
	// 自機の表示位置/サイズ
	SDL_FRect dstRect;
	// 自機の有効化
	bool isActive;
	// 表示させる弾の数
	std::vector<Bullet> bulletNum;
};
Plane.cpp
// ----------  ライブラリ  ----------
#include "Plane.h"

// ----------  定数  ----------
// 自機の初期生成位置
constexpr float SPAWN_X    = 144.0f;
constexpr float SPAWN_Y    = 544.0f;
// 進行方向に進む距離
constexpr float MOVE_SPEED =  5.0f;

// ----------  メソッド  ----------
// 初期化関数
void Plane::init()
{
	// 自機のコマ送りのカウントの初期化
	frameCount = 3;

	// 画像の切り出し位置/サイズの初期化
	srcRect = {
		.x = 0.0f,
		.y = 0.0f,
		.w = CHARACTER_SIZE,
		.h = CHARACTER_SIZE
	};

	// 自機の表示位置/サイズの初期化
	dstRect = {
		.x = SPAWN_X,
		.y = SPAWN_Y,
		.w = CHARACTER_SIZE,
		.h = CHARACTER_SIZE
	};

	// 自機の有効化
	isActive = true;

	// 弾の数をクリア
	bulletNum.clear();
}

// コマ送り/弾の管理用の関数
void Plane::update()
{
	if( !isActive ) { return; }

	// 自機のコマ送り
	srcRect.x = (float)((frameCount / ANIME_SPEED) % FRAME) * CHARACTER_SIZE;
	frameCount++;

	// 弾の表示位置変更
	for( Bullet &bullet : bulletNum )
	{
		bullet.update();
	}

	// 使わなくなった弾の掃除
	for( int i = (int)bulletNum.size() - 1; i >= 0 ; i-- )
	{
		if( !bulletNum[i].getIsActive() )
		{
			bulletNum.erase(bulletNum.begin() + i);
			
		}
	}
}

// 進行方向に位置を変更/壁の当たり判定用の関数
void Plane::move(char direct)
{
	if( !isActive ) { return; }

	// 進行方向に進む処理
	switch( direct )
	{
		// 上方向
		case 1:
			dstRect.y -= MOVE_SPEED;
			break;
		// 右方向
		case 2:
			dstRect.x += MOVE_SPEED;
			break;
		// 下方向
		case 3:
			dstRect.y += MOVE_SPEED;
			break;
		// 左方向
		case 4:
			dstRect.x -= MOVE_SPEED;
			break;
	}

	// 壁の当たり判定
	// 上壁
	if( dstRect.y < 0 )
	{
		dstRect.y = 0;
	}
	// 右壁
	if( dstRect.x > (WINDOW_WIDTH - CHARACTER_SIZE) )
	{
		dstRect.x = (WINDOW_WIDTH - CHARACTER_SIZE);
	}
	// 下壁
	if( dstRect.y > (WINDOW_HEIGHT - CHARACTER_SIZE) )
	{
		dstRect.y = (WINDOW_HEIGHT - CHARACTER_SIZE);
	}
	// 左壁
	if( dstRect.x < 0 )
	{
		dstRect.x = 0;
	}
}

// 表示用の関数
void Plane::draw(SDL_Renderer *renderer, SDL_Texture *spritesheet)
{
	// 弾の表示
	for( Bullet &segment : bulletNum )
	{
		segment.draw(renderer, spritesheet);
	}

	// バックバッファへ書き込み
	SDL_RenderTexture(renderer, spritesheet, &srcRect, &dstRect);
}

// 自機のx値を返す関数
float Plane::getX()
{
	return dstRect.x;
}

// 自機のy値を返す関数
float Plane::getY()
{
	return dstRect.y;
}

// 自機の無効化用の関数(敵に当たったとき)
void Plane::setIsNonActive()
{
	isActive = false;
}

// 表示させる弾を保存する関数
void Plane::setBullet(Bullet &bullet)
{
	bulletNum.push_back(bullet);
}

// 弾のリストを返す関数
std::vector<Bullet>& Plane::getBullet()
{
	return bulletNum;
}

// 自機の位置/サイズを返す関数
SDL_FRect Plane::getRect()
{
	return { dstRect.x, dstRect.y, dstRect.w, dstRect.h };
}

Bullet.cpp

Bullet.h
#pragma once // BULLET_H

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

#include "Config.h"

// -----------  クラス  -----------
class Bullet
{
public:
	// 初期化関数
	void init(float x, float y);
	// 弾の位置の更新/表示寿命の管理用の関数
	void update();
	// 表示用の関数
	void draw(SDL_Renderer *renderer, SDL_Texture *splitesheet);
	// 弾の有効/無効を返す関数
	bool getIsActive();
	// 弾の無効化用の関数(敵に当たったとき)
	void setIsNonActive();
	// 弾の位置/サイズを返す関数
	SDL_FRect getRect();
private:
	// 画像の切り出し位置/サイズ
	SDL_FRect srcRect;
	// 弾の表示位置/サイズ
	SDL_FRect dstRect;
	// 弾の有効化
	bool isActive;
};
Bullet.cpp
// ----------  ライブラリ  ----------
#include "Bullet.h"

// ----------  定数  ----------
// 弾のスピード
constexpr float BULLET_SPEED  =   5.0f;
// 弾の表示寿命の位置
constexpr float DRAW_LIMIT    = -11.0f;
// 弾の切り出し位置/サイズor表示サイズ
constexpr float BULLET_X      = 110.0f;
constexpr float BULLET_Y      =  11.0f;
constexpr float BULLET_WIDTH  =   4.0f;
constexpr float BULLET_HEIGHT =  11.0f;

// ----------  メソッド  ----------
// 初期化関数
void Bullet::init(float x, float y)
{
	// 画像の切り出し位置/サイズの初期化
	srcRect = {
		.x = BULLET_X,
		.y = BULLET_Y,
		.w = BULLET_WIDTH,
		.h = BULLET_HEIGHT
	};

	// 自機の表示位置/サイズの初期化
	dstRect = {
		.x = x + (int)(CHARACTER_SIZE / 2 - 1),
		.y = y - 11.0f,
		.w = BULLET_WIDTH,
		.h = BULLET_HEIGHT
	};

	// 弾の有効化
	isActive = true;
}

// 弾の位置の更新/表示寿命の管理用の関数
void Bullet::update()
{
	if( !isActive ) { return; }

	// 弾を上に飛ばす
	dstRect.y -= BULLET_SPEED;

	// 表示限界の判定
	if( dstRect.y <= DRAW_LIMIT )
	{
		isActive = false;
	}
}

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

	// バックバッファへ書き込み
	SDL_RenderTexture(renderer, texture, &srcRect, &dstRect);
}

// 弾の有効/無効を返す関数
bool Bullet::getIsActive()
{
	return isActive;
}

// 弾の無効化用の関数(敵に当たったとき)
void Bullet::setIsNonActive()
{
	isActive = false;
}

// 弾の位置/サイズを返す関数
SDL_FRect Bullet::getRect()
{
	return { dstRect.x, dstRect.y, dstRect.w, dstRect.h };
}

Enemy.cpp

Enemy.h
#pragma once // ENEMY_H

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

#include "Config.h"

// ----------  クラス  ----------
class Enemy
{
public:
	// 初期化関数
	void init();
	// コマ送り/移動位置の更新用の関数
	void update();
	// 表示用の関数
	void draw(SDL_Renderer *renderer, SDL_Texture *spritesheet);
	// 弾の有効/無効を返す関数
	bool getIsActive();
	// 敵の無効化用の関数(弾に当たったとき)
	void setIsNonActive();
	// 敵の位置を返す関数
	float getY();
	// 敵の位置/サイズを返す関数
	SDL_FRect getRect();
private:
	// 1フレームごとにカウント
	unsigned int frameCount;
	// 画像の切り出し位置/サイズ
	SDL_FRect srcRect;
	// 敵の表示位置/サイズ
	SDL_FRect dstRect;
	// 敵の有効化
	bool isActive;
};
Enemy.cpp
// ----------  ライブラリ  ----------
#include <random>

#include "Enemy.h"

// ----------  定数  ----------
constexpr float ENEMY_SPEED = 1.0f;

// ----------  メソッド  ----------
// 初期化関数
void Enemy::init()
{
	// 乱数生成器
	static std::random_device rd;
	static std::mt19937_64 gen(rd());
	// 範囲指定
	std::uniform_real_distribution<float> dist_x(CHARACTER_SIZE, WINDOW_WIDTH - CHARACTER_SIZE);

	// 敵のコマ送りのカウントの初期化
	frameCount = 3;

	// 画像の切り出し位置/サイズの初期化
	srcRect = {
		.x = CHARACTER_SIZE * 4,
		.y = 0.0f,
		.w = CHARACTER_SIZE,
		.h = CHARACTER_SIZE
	};

	// 敵の表示位置/サイズの初期化
	dstRect = {
		.x = dist_x(gen),
		.y = -CHARACTER_SIZE,
		.w = CHARACTER_SIZE,
		.h = CHARACTER_SIZE
	};

	// 敵の有効化
	isActive = true;
}

// コマ送り/移動位置の更新用の関数
void Enemy::update()
{
	if( !isActive ){ return; }

	// 敵のコマ送り
	srcRect.x = (float)((CHARACTER_SIZE * 4) + ((frameCount / ANIME_SPEED) % FRAME) * CHARACTER_SIZE);
	frameCount++;

	// ゆらゆら動きの再現
	if( (frameCount / ANIME_SPEED) % 2 == 0 )
	{
		dstRect.x -= ENEMY_SPEED;
	}
	else
	{
		dstRect.x += ENEMY_SPEED;
	}
	// 敵の降りてくるスピード
	dstRect.y += ENEMY_SPEED;
}

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

	// バックバッファへ書き込み
	SDL_RenderTexture(renderer, spritesheet, &srcRect, &dstRect);
}

// 敵の有効/無効を返す関数
bool Enemy::getIsActive()
{
	return isActive;
}

// 敵の無効化用の関数(弾に当たったとき)
void Enemy::setIsNonActive()
{
	isActive = false;
}

// 敵のy値を返す関数
float Enemy::getY()
{
	return dstRect.y;
}

// 敵の位置/サイズを返す関数
SDL_FRect Enemy::getRect()
{
	return { dstRect.x, dstRect.y, dstRect.w, dstRect.h };
}
0
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
0
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?