"Good morning, and in case I don't see ya, good afternoon, good evening, and good night!" 映画「トゥルーマン・ショー」より
本記事は、アドベントカレンダーほぼ厚木の民の記事です。
12日目の担当のIshidaです。よろしくお願いします。
何を題材にしようかと悩みましたが、
今回は、私が学生時代に実際に作ったPCゲームに関して記事を書いていこうと思います。
ゲーム作りのきっかけ
私自身、学生時代はコンピュータ・グラフィックス系の研究室に所属していました。
その当時、表向きは、今は亡きNVIDIAのCgや、OpenGLなどを使ってアプリ開発や研究に勤しんでましたが、裏では、研究室メンバとレトロなPCゲームづくりをやりながら遊んでいる日々でした。
まあ、レトロという名の”クソゲー”です。
楽しみながらプログラミングの勉強ができていたのは、懐かしい良き思い出です。
この機会に、10年ぶりくらいに学生自体に使っていたHDDを掘り出して、実際に作ったゲームをいくつか動かしてみました。
残念なことに、まともに動かせそうなのが初期に作ったゲームしかなかったので、こちらを題材にコードを簡単に読解していきながら動かしていきたいと思います。
利用するAPI・動作環境
最近はゲーム向けのSDK・Platform・APIが豊富です。
・Unity
・Unreal Engine
・Cocos2d-x
・CryEngine
ただ、(期待していた方がいらっしゃったら申し訳ないですが、)今回はこういったハイクオリティなものは使わないです。
今回は、Playstationにも使われているMicrosoft DirectXと言われるAPIを使っていきます。
DirectXは、Microsoftが開発したゲーム・マルチメディア向け処理のAPI群です。
Windows10にも一部入っているようで、現在のバージョンは12らしいです。
そのため、今回の開発・動作PC環境もWindows前提、開発はVisualStudioを使っていきます。
また、言語はC/C++です。
DirectXのインストールの仕方などは割愛します。
- 実装に関しては、このあたりを参考にしていた記憶があります。
ゲームをつくろう!
私が最初に作ったゲームは、いわゆるシューティング。いわゆるインベーダーゲームです。
スペースインベーダーとして株式会社タイトーが1978年に発売したアーケードゲームです。
ただ今回は、スコア計算やランキングなんて機能は作りません。ただ敵を”打つ””倒す”のみです。もちろん2Dです。
ゲーム作りは、どういったゲームにするのかなど決めながら実装していくのが楽しいので、実装しながら決めていきたいと思います。
ベースの仕様決め
いわゆるシューティングゲームを前提として、ベースの仕様は最初に決めていきたいと思います。
まずは、画面遷移です。
いきなりゲーム画面を表示させても良いですが、メニュー画面を作るだけでゲームのクオリティが格段に上がるので、メニュー画面も作ります。
画面としては、以下の2つにします。
・メニュー画面
・プレイ画面
画面遷移としては、書くまでもないですが、以下のようします。
[メニュー画面] ⇔ [プレイ画面]
ただし、プレイ画面からメニュー画面に遷移する場合は、GameOverとGameClearで表示画面を少し変えたいと思います。
次に、キャラクターです。
シューティングなので、プレイヤーが撃った弾の仕様も決めていきます。
キャラクターは、以下の2つにします。
・プレイヤー+弾
・敵キャラ
それぞれのキャラクターと弾は、画面の位置情報を持っていることにします。
敵キャラは数体配置しましょう。
そして、書くまでもないですが、ゲームのルールとしては、プレイヤーから弾を撃って、敵キャラに当たったら、倒したことにします。敵キャラをすべて倒したらGameClear、敵キャラが画面下まで到着したらGameOverにします。
ベースの仕様はこれくらいで良いでしょう。実際に実装していきます。
ゲームの外枠の作成
まずは、ゲームの外枠としてウインドウ画面を作っていきます。
ただのウインドウをDirectXで出力するくらいなら、6行くらいで書けます。
#include "DxLib.h"
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){
ChangeWindowMode(TRUE);
DxLib_Init();
SetDrawScreen(DX_SCREEN_BACK);
while(1) {
}
DxLib_End();
return 0;
}
while(){}の中に、実際の処理を追加していくことになります。
メニュー画面の追加
画面を追加してみます。
まずは、メニュー画面の部分のみ書いてみます。
SCENE gameMenu(int flag) {
int FontTitle = CreateFontToHandle("メイリオ", 90, 3, DX_FONTTYPE_ANTIALIASING_EDGE);
int FontSub = CreateFontToHandle("メイリオ", 70, 3, DX_FONTTYPE_ANTIALIASING_EDGE);
int sel = 0;
while (!ScreenFlip() && !ClearDrawScreen()){
Keyboard_Update();
DrawStringToHandle(50, 100, "INVADERS GAME", GetColor(255, 255, 255), FontTitle);
if (flag == 0) {
DrawString(300, 240, "Play", 255);
DrawString(300, 256, "Exit", 255);
}
else if (flag == 1) {
DrawStringToHandle(150, 350, "GAME CLEAR!", GetColor(255, 255, 255), FontSub);
DrawString(300, 240, "Retry", 255);
DrawString(300, 256, "Exit", 255);
}
else if (flag == -1) {
DrawStringToHandle(150, 350, "GAME OVER!", GetColor(255, 0, 0), FontSub);
DrawString(300, 240, "Retry", 255);
DrawString(300, 256, "Exit", 255);
}
if (Keyboard_Get(KEY_INPUT_UP) == 1) sel = 0;
if (Keyboard_Get(KEY_INPUT_DOWN) == 1) sel = 1;
if (Keyboard_Get(KEY_INPUT_SPACE) == -1) break;
DrawBox(0, sel * 16 + 240, 640, (sel + 1) * 16 + 240, 256 * 255, false);
if (ProcessMessage() == -1) return end;
}
DeleteFontToHandle(FontTitle);
DeleteFontToHandle(FontSub);
return sel ? SCENE::end : SCENE::play;
}
詳細の関数は、各自確認してください。
Keyboardモジュールに関しての説明も、今回は割愛します。
メニュー画面では、キーボードの「↑」「↓」でPlayとExitを選択できるように実装しました。
また、gameMenu(int flag)の引数が0の場合は初期メニュー、1の場合はGame Clear、-1の場合はGame Overを追加で表示させているだけです。
いい感じにクソゲーレトロ感溢れる外枠ができました。
外枠ができたので、次にゲームの中身を実装していきます。
ゲームキャラクターの実装
まずは、キャラクターのオブジェクトを書いていきます。
プレイヤーオブジェクト、弾オブジェクト、敵キャラオブジェクトの順に実装していきます。
最初にプレイヤーを実装していきます。
#ifndef DEF_PLAYER_H
#define DEF_PLAYER_H
typedef struct{
int Image;
int x;
int y;
} Player_t;
// 初期化をする
void Player_Initialize(Player_t *Player, int x, int y);
// 動きを計算する
int Player_Calc(Player_t *Player);
// 描画する
void Player_Graph(Player_t Player);
// 終了処理をする
void Player_Finalize(Player_t Player);
#endif
#include "DxLib.h"
#include "Player.h"
#include "Keyboard.h"
// 初期化をする
void Player_Initialize(Player_t *Player, int x, int y){
Player->Image = LoadGraph("images/bar.png");
Player->x = x;
Player->y = y;
}
// 動きを計算する
int Player_Calc(Player_t *Player){
if(Keyboard_Get(KEY_INPUT_RIGHT) > 0){
Player->x+=3;
}
if(Keyboard_Get(KEY_INPUT_LEFT) > 0){
Player->x-=3;
}
return (Player->x);
}
// 描画する
void Player_Graph(Player_t Player){
DrawGraph( Player.x, Player.y, Player.Image, TRUE );
}
// 終了処理をする
void Player_Finalize(Player_t Player){
DeleteGraph( Player.Image );
}
やっていることは、コメントの通りです。
DirectXでは、DwarGraph関数で簡単に描画ができますので便利です。
少し補足すると、プレイヤーはキーボードの「←」「→」だけ移動できるようにします。
次に、プレイヤーが放つ弾の実装をします。
#ifndef DEF_SHOT_H
#define DEF_SHOT_H
typedef struct{
int Image;
int x;
int y;
int flag;
}Shot_t;
// 初期化をする
void Shot_Initialize(Shot_t *Shot, int x, int y);
// 動きを計算する
int Shot_Calc(Shot_t *Shot, int player_x);
// 描画する
void Shot_Graph(Shot_t Shot);
// 終了処理をする
void Shot_Finalize(Shot_t SHot);
#endif
#include "DxLib.h"
#include "Game.h"
#include "Shot.h"
#include "Keyboard.h"
#include "Player.h"
// 初期化をする
void Shot_Initialize(Shot_t *Shot, int x, int y){
//初期化処理
Shot->Image = LoadGraph("images/shot00.png");
Shot->x = x;
Shot->y = y;
Shot->flag = 0; //飛んでいない事を示すフラグ=0
}
// 動きを計算する
int Shot_Calc(Shot_t *Shot, int player_x){
if(Keyboard_Get(KEY_INPUT_SPACE) > 0 && !Shot->flag){
Shot->flag = 1;
}
else if(Shot->flag){
Shot->y -= 5;
}
else if(!Shot->flag){
Shot->x = player_x;
Shot->y = PLAYER_POS_Y;
}
if(Shot->y == 0){
Shot->flag = 0;
}
return Shot->x, Shot->y;
}
// 描画する
void Shot_Graph(Shot_t Shot){
if(Shot.flag)
DrawGraph(Shot.x, Shot.y, Shot.Image, TRUE);
}
// 終了処理をする
void Shot_Finalize(Shot_t Shot){
DeleteGraph(Shot.Image);
}
キーボードの「SPACE」押下のタイミングで、プレイヤーの位置から一直線に進んでいく動きをさせます。
本家のインベーダーゲームはどうだか忘れてしまいましたが、1発のみしか打てない仕様にしています。
弾の速度・散布率など色々工夫できるところが楽しいですが、今回はこれでいきます。
最後に敵キャラを実装します。
#ifndef DEF_ENEMY_H
#define DEF_ENEMY_H
#include "Shot.h"
typedef struct{
int Image;
int x;
int y;
int state; // 0=nomal, 1-10=hit, 11=Black, 12=GameOver
int rl; // move direction
int e_count; // direction counter
} Enemy_t;
// 初期化をする
void Enemy_Initialize(Enemy_t *Enemy, int x, int y);
// 動きを計算する
int Enemy_Calc(Enemy_t *Enemy, Shot_t *s);
// 描画する
void Enemy_Graph(Enemy_t Enemy);
// 終了処理をする
void Enemy_Finalize(Enemy_t Enemy);
#endif
#include "DxLib.h"
#include "Game.h"
#include "Enemy.h"
#include "Keyboard.h"
#include "Shot.h"
#include <stdlib.h>
// 初期化をする
void Enemy_Initialize(Enemy_t *Enemy, int x, int y){
Enemy->Image = LoadGraph("images/ufo.png");
Enemy->x = x;
Enemy->y = y;
Enemy->state = 0;
Enemy->e_count = 0;
Enemy->rl = 0;
}
// 動きを計算する
int Enemy_Calc(Enemy_t *Enemy, Shot_t *shot){
int step = 10;
// move
if(Enemy->state == 0){
if (Enemy->e_count > step + rand()%5){
Enemy->y++;
Enemy->rl++;
Enemy->e_count = 0;
}
else if (Enemy->rl % 2 == 0){
Enemy->x++;
if (Enemy->x > WIDTH)
Enemy->x = WIDTH - 1;
Enemy->e_count++;
}
else if (Enemy->rl % 2 == 1){
Enemy->x--;
if (Enemy->x < 0)
Enemy->x = 0 + 1;
Enemy->e_count++;
}
}
// Hit
if (Enemy->state == 0) {
if (Enemy->x < shot->x + 7 && shot->x + 7 < Enemy->x + 21
&& Enemy->y < shot->y + 5 && shot->y + 5 < Enemy->y + 16
&& Enemy->x > 0 && Enemy->x < WIDTH
&& Enemy->y > 0 && Enemy->y < HEIGHT){
shot->flag = 0;
Enemy->Image = LoadGraph("images/hit.png");
Enemy->state = 1;
}
}
// bom
if(Enemy->state > 0 && Enemy->state < 10){
Enemy->state++;
if(Enemy->state == 10){
Enemy->Image = LoadGraph("images/black.png");
Enemy->state = 11;
}
}
// line over
if (Enemy->state == 0 && Enemy->y > HEIGHT)
Enemy->state = 12;
return Enemy->state;
}
// 描画する
void Enemy_Graph(Enemy_t Enemy){
DrawGraph( Enemy.x, Enemy.y, Enemy.Image, TRUE);
}
// 終了処理をする
void Enemy_Finalize(Enemy_t Enemy){
DeleteGraph(Enemy.Image );
}
敵キャラは、少しだけ複雑です。
Rand関数を一部使って左右に振れながらだんだん近づいてくるように動かします。
このあたりの敵の動かせ方は、色々工夫が一番でき、ゲーム開発の醍醐味ですが、今回はこれで良しとします。
また、プレイヤーが放った弾に触れたタイミングで、爆発→blankに変化するようにStatusを以降させます。
そして、敵キャラが画面下部まで到達できてしまった状態になったら、敵キャラのStatusをGameOverに変更させます。
ここまでで、キャラクタのオブジェクト作成は完了したので、次はプレイ画面を作って、そこにキャラクタを配置させたいと思います。
プレイ画面の追加
プレイ画面を追加します。
メニュー画面とは少し異なり、キャラクタの初期位置を指定していきます。
#define Enemy_Number_x 8
#define Enemy_Number_y 8
SCENE gamePlay() {
bool status = false;
Enemy_t Enemy[Enemy_Number_x * Enemy_Number_y];
Player_t Player;
Shot_t shot;
static int counter;
// 敵の初期化
for (int i = 0; i<Enemy_Number_x; ++i) {
for (int j = 0; j < Enemy_Number_y; ++j) {
Enemy_Initialize(Enemy + i + Enemy_Number_x * j, i * 80 + 20, j * 80 - 480);
}
}
Player_Initialize(&Player, 280, 450);
Shot_Initialize(&shot, 280, 450);
// 処理
while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){
Keyboard_Update();
Player_Calc(&Player);
Player_Graph(Player);
counter=0;
if(!counter){
Shot_Calc(&shot, Player_Calc(&Player));
Shot_Graph(shot);
}
counter++;
bool exist=true;
for(int i=0;i<SIZE(Enemy);++i) {
int e_stat = Enemy_Calc(Enemy + i, &shot);
if (e_stat == 12)
status = true;
else
exist &= e_stat;
Enemy_Graph(Enemy[i]);
}
// GameClear
if (exist == true)break;
if (status == true)break;
}
Shot_Finalize(shot);
for(int i=0;i<SIZE(Enemy);++i) {
Enemy_Finalize(Enemy[i]);
}
Player_Finalize(Player);
if (status == false)
return SCENE::clear;
else
return SCENE::over;
}
まず、敵キャラを配置、プレイヤーを配置します。
あとは、すべての敵キャラのStatusがBlankになるか、1体でも敵キャラのStatusがGameOverになるまでWhileでループさせます。
、、、コードはもう少しきれいに書けそうですが許してください。
画面遷移の結合
あとは、メイン関数を少し変更して、画面遷移を結合させます。
enum SCENE{
menu,
clear,
over,
play,
end
};
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){
ChangeWindowMode(TRUE);
DxLib_Init();
SetDrawScreen(DX_SCREEN_BACK);
SCENE s = menu;
while(s!=SCENE::end) {
switch (s) {
case SCENE::menu:
s = gameMenu(0); break;
case SCENE::play:
s = gamePlay(); break;
case SCENE::clear:
s = gameMenu(1); break;
case SCENE::over:
s = gameMenu(-1); break;
}
}
DxLib_End();
return 0;
}
実際のゲームの様子はこんな感じです。
— tskisds (@tskisds) January 7, 2021
3分くらいで全滅させることができました。
※Twitterの動画Upload制約の都合上、1.4倍速で再生してます。
以下に、コードをアップロードしましたので、興味のある方はダウンロードしてください。
※すみません、後ほど対応します。
最後に
遊びながらプログラミングしてるのが一番楽しいですね。
今回は、シンプルな機能しかありませんが、敵キャラの動きを変化させたり、撃てる弾の種類を増やしたりするだけで実装の楽しさが倍増します。よくあるイースターエッグ的な機能も付けやすいです。楽しいです。
、、、だからといって、それが仕事になっちゃうと「あれ」なんですけど。
そんな感じで。お粗末な文章ですみません、、、
みなさま、良いお年をお迎えください。
"Good morning, and in case I don't see ya, good afternoon, good evening, and good night!" 映画「トゥルーマン・ショー」より