#これまでのあらすじ
- ゲームの骨格ができた
- あとは弾幕シューティングをつくるだけ
4日目はここです。
#5日目
弾幕シューティング開発5日目になりました。今日からついに弾幕シューティングのメインの部分の開発にはいります。
...入りたかった。
というのも前回までで作ったゲームの骨格に少し改良が必要なので、まずはいろいろな機能の改修をします。
その後で、実際に弾幕シューティングを作っていきます。まずは何といっても自機キャラクターを描画して実際に動かしてみるところまでやりたいと思います。
それでは頑張っていきます。
#前回までのプログラムの改修
見つかった改修点は1つじゃなかった...。というわけで今回改修する点がこちら。
- ゲーム開始からの時間の取得
- 方向キーなど特殊キーの入力
- fps制御の待機時間を求める処理
##ゲーム開始からの時間取得方法
結論から言うと、この処理はglutに備わっていました。つまり、自作した時間を求める関数は必要なかったわけです。その関数はglutGet関数です。この関数の引数にGLUT_ELAPSED_TIMEを渡すと、返り値として実行開始または初めてglutGet関数が呼ばれてからの経過時間がミリ秒単位で取得できます。これが欲しかったんや...。というわけで自作したtimeUtilsGetMilliSeconds関数の部分をglutGet(GLUT_ELAPSED_TIME)に直します。
##方向キーの入力監視処理
これはすっかり忘れていました。ASCIIコードに方向キーなんてもん含まれていません。というわけで実際に改修したコードがこれ。
//以下を追記
#define KEY_UP 128
#define KEY_DOWN 129
#define KEY_LEFT 130
#define KEY_RIGHT 131
#include "key.h"
#include <GL/glut.h>
#include <stdio.h>
static unsigned char keys[256];
void keyboard(unsigned char key, int x, int y);
void keyboardUp(unsigned char key, int x, int y);
void specialKey(int key, int x, int y);
void specialKeyUp(int key, int x, int y);
void keyInit(){
glutKeyboardFunc(keyboard);
glutKeyboardUpFunc(keyboardUp);
glutSpecialFunc(specialKey);
glutSpecialUpFunc(specialKeyUp);
glutIgnoreKeyRepeat(GL_TRUE);
for (int i = 0; i < 256; i++)
keys[i] = 0;
}
int keyGetState(unsigned char key){
return keys[key];
}
void keyboard(unsigned char key, int x, int y){
//printf("keyboard: \"%c\"(%#x)\n", key, key);
keys[key] = 1;
}
void keyboardUp(unsigned char key, int x, int y){
//printf("keyboardUp: \"%c\"(%#x)\n", key, key);
keys[key] = 0;
}
void specialKey(int key, int x, int y){
if (key == GLUT_KEY_UP) {
keys[KEY_UP] = 1;
}
if (key == GLUT_KEY_DOWN) {
keys[KEY_DOWN] = 1;
}
if (key == GLUT_KEY_LEFT) {
keys[KEY_LEFT] = 1;
}
if (key == GLUT_KEY_RIGHT) {
keys[KEY_RIGHT] = 1;
}
}
void specialKeyUp(int key, int x, int y){
if (key == GLUT_KEY_UP) {
keys[KEY_UP] = 0;
}
if (key == GLUT_KEY_DOWN) {
keys[KEY_DOWN] = 0;
}
if (key == GLUT_KEY_LEFT) {
keys[KEY_LEFT] = 0;
}
if (key == GLUT_KEY_RIGHT) {
keys[KEY_RIGHT] = 0;
}
}
特殊キーの監視には、glutSpecialFunc関数とglutSpecialUpFunc関数にそれぞれキーが押されているときの処理をする関数を登録することで実現します。やっていることは、ほかのキーが押されたときと変わりません。
##fps制御の挙動
前回までに作ったfps制御を実際に動かすと、待機時間が正確に求められない時が出てきました。何が問題かというと、求められた待機時間がやたら長く、描画がかくつく時がそこそこの頻度ででます。具体的な値としては、現状のプログラムでの1フレーム当たりの待機時間は長くて15とか16ミリ秒だと思われるのに、25ミリ秒や30ミリ秒を超えるような待機時間が算出されます。しかも原因がわかりません。glutIdleFuncに登録したコールバック関数gameLoopの中でfpsWait関数を呼んでいるわけですが、gameLoop関数が呼ばれる頻度に多少ばらつきがあっても大丈夫な設計だと思っていたのですが...。とりあえず応急処置的なコードを追加して問題はでなくなりました。そのコードがこちら。
#include "fps.h"
#include <unistd.h>
#include "font.h"
#include <GL/glut.h>
//#include <stdio.h>
#define FPS 60
#define UPDATE_INTERVAL 60
#define STOCK_MAX 120
typedef struct stock_ {
unsigned int m_d[STOCK_MAX];
int m_pos;
int m_len;
int m_oldestPos;
} stock_t;
static int count;
static float fps;
static stock_t stockTime;
unsigned int waitTime();
void regist();
void updateFps();
void fpsInit(){
count = 0;
fps = 0.0f;
stockTime.m_pos = 0;
stockTime.m_len = 0;
stockTime.m_oldestPos = 0;
for (int i = 0; i < STOCK_MAX; i++)
stockTime.m_d[i] = 0;
}
void fpsWait(){
count++;
usleep(waitTime() * 1000);
regist();
if (count == UPDATE_INTERVAL){
updateFps();
count = 0;
}
}
float fpsGet(){
return fps;
}
unsigned int waitTime(){
int len = stockTime.m_len;
if (len == 0)
return 0;
float calcTime = 1000.f / (float)FPS * ((float)len);
int realTime =
glutGet(GLUT_ELAPSED_TIME) - (int)stockTime.m_d[stockTime.m_oldestPos];
float wait = calcTime - (float)realTime;
//printf("%.2f\n", wait);
if (wait > 17){
wait = 17;
}
if (wait < 0) wait = 0;
return (unsigned int)wait;
}
void regist(){
stockTime.m_d[stockTime.m_pos] = (unsigned int)glutGet(GLUT_ELAPSED_TIME);
stockTime.m_pos++;
if (stockTime.m_pos == STOCK_MAX)
stockTime.m_pos = 0;
if (stockTime.m_len < STOCK_MAX)
stockTime.m_len++;
else
stockTime.m_oldestPos = stockTime.m_pos;
}
void updateFps(){
int len = stockTime.m_len;
if (len < STOCK_MAX)
return;
const int op = stockTime.m_oldestPos;
unsigned int realTime = op == 0 ?
stockTime.m_d[STOCK_MAX - 1] - stockTime.m_d[0] :
stockTime.m_d[op - 1] - stockTime.m_d[op];
float average = (float)realTime / (float)(STOCK_MAX - 1);
if (average == 0)
return;
fps = 1000.f / average;
}
追加したのはwaitTime関数の中です。待機時間が17ミリ秒より大きく求まった場合は待機時間を17ミリ秒にするようにしました。誤差の蓄積でも25ミリ秒待機とかにはならないと思うんですが...。まあ、これから処理をどんどん追加していったら待機時間を多く要するなんてことにはならなくなると思います。原因がわかりましたら、また報告します。
#弾幕シューティング開発
とりあえず改修は済んだので、いよいよ弾幕シューティング開発の第一歩です。今日やることは、自機キャラの描画と移動までの実装です。1日目でも書きましたが、Solid Aetherのような見た目を目指します。理由は様々ですが、1つは画像をたくさん用意する必要がないこと、矩形や円の描画はglutの機能を使うことで簡単に実現できることが大きな理由です。
とというわけで、まずは円と矩形を描画するための機能を用意します。その後で自機キャラを描画し実際に動かしてみます。
#今日の目標
- いくつかの機能の改修(済)
- 矩形と円の描画処理を作成
- 自機キャラの見た目を決めて描画
- 自機キャラを動かす
#矩形と円の描画処理
描画処理はshapeというディレクトリの中に入れておきます。
src/
+- ...
+- shape/
+- rect.c
+- rect.h : 矩形の描画
+- ball.c
+- ball.h : 円の描画
##矩形
まずは矩形の描画機能を作ります。
#ifndef ___HEADER_RECT
#define ___HEADER_RECT
void rectDraw(float x,
float y,
float width,
float height,
float angle,
unsigned char red,
unsigned char green,
unsigned char blue);
#endif
#include "rect.h"
#include <GL/glut.h>
void rectDraw(float x, float y, float width, float height, float angle, unsigned char red, unsigned char green, unsigned char blue){
glPushMatrix();
glTranslatef(x, y, 0.0f);
glColor3ub(red, green, blue);
glRotatef(angle, 0.0f, 0.0f, 1.0f);
glRectf(-width / 2.0f, -height / 2.0f, width / 2.0f, height / 2.0f);
glPopMatrix();
}
矩形の描画にはglRectf関数を用います。rectDraw関数の引数は矩形の中心座標x、y、矩形の横幅と縦幅width、height、矩形の傾きangle、矩形の色rgbです。
##円
次に円の描画機能です。
#ifndef ___HEADER_BALL
#define ___HEADER_BALL
void ballDraw(float x,
float y,
float r,
unsigned char red,
unsigned char green,
unsigned char blue);
#endif
#include "ball.h"
#include <GL/glut.h>
void ballDraw(float x, float y, float r, unsigned char red, unsigned char green, unsigned char blue){
glPushMatrix();
glTranslatef(x, y, 0.0f);
glColor3ub(red, green, blue);
glScalef(r, r, 0.0f);
glutSolidSphere(1, 16, 16);
glPopMatrix();
}
円の描画には、球を描画するglutSolodSphere関数を用いました。z軸方向への拡大をなくし、三次元の球を二次元の円のように見せることで実現しました。ballDraw関数の引数は、円の中心座標x、y、円の半径r、円の色rgbです。glutSolidSphere関数を使うことでこの先動作が重いと感じられる場合は、glutを使って円を描画する一般的な方法であるポリゴンを組み合わせる描画方法に変えるかもしれません。
これでゲーム中で描画される図形の描画機能が完成しました。
#自機キャラの描画
##自機キャラの見た目
こんな感じにします。
●◇● [自機]
中央は正方形で、常にゆっくり回転させます。
自機には低速モードがあって、低速モードの状態では正方形の中央に小さな円を表示させます。
##自機キャラの実装
それでは実装していきます。ソースの場所は以下の通りです。
src/
+- ...
+- player.h
+- player.c : 自機キャラ
#ifndef ___HEADER_PLAYER
#define ___HEADER_PLAYER
typedef struct player_ {
float m_x;
float m_y;
int m_slow;
} player_t;
void playerInit(player_t *player);
void playerUpdate(player_t *player);
void playerDraw(const player_t *player);
#endif
#include "player.h"
#include "key.h"
#include <math.h>
#include <GL/glut.h>
#include "./shape/rect.h"
#include "./shape/ball.h"
static const float PLAYER_SPEED = 6.0f;
static const float PLAYER_RECT_SIZE = 15.0f;
static const unsigned char PLAYER_RECT_COLOR[3] = {230, 230, 230};
static const float PLAYER_CENTER_BALL_R = 4.0f;
static const unsigned char PLAYER_CENTER_BALL_COLOR[3] = {255, 100, 100};
static const float PLAYER_SIDE_BALL_POSITION = 16.0f;
static const float PLAYER_SIDE_BALL_R = 4.5f;
static const unsigned char PLAYER_SIDE_BALL_COLOR[3] = {64, 64, 255};
static const float PLAYER_FIRST_POSITION_X = 200.0f;
static const float PLAYER_FIRST_POSITION_Y = 400.0f;
static int count;
void move(player_t *player);
void playerInit(player_t *player){
player->m_x = PLAYER_FIRST_POSITION_X;
player->m_y = PLAYER_FIRST_POSITION_Y;
count = 0;
}
void playerUpdate(player_t *player){
move(player);
count++;
}
void playerDraw(const player_t *player){
rectDraw(player->m_x,
player->m_y,
PLAYER_RECT_SIZE,
PLAYER_RECT_SIZE,
(float)(count % 360),
PLAYER_RECT_COLOR[0],
PLAYER_RECT_COLOR[1],
PLAYER_RECT_COLOR[2]);
if (player->m_slow)
ballDraw(player->m_x,
player->m_y,
PLAYER_CENTER_BALL_R,
PLAYER_CENTER_BALL_COLOR[0],
PLAYER_CENTER_BALL_COLOR[1],
PLAYER_CENTER_BALL_COLOR[2]);
ballDraw(player->m_x - PLAYER_SIDE_BALL_POSITION,
player->m_y,
PLAYER_SIDE_BALL_R,
PLAYER_SIDE_BALL_COLOR[0],
PLAYER_SIDE_BALL_COLOR[1],
PLAYER_SIDE_BALL_COLOR[2]);
ballDraw(player->m_x + PLAYER_SIDE_BALL_POSITION,
player->m_y,
PLAYER_SIDE_BALL_R,
PLAYER_SIDE_BALL_COLOR[0],
PLAYER_SIDE_BALL_COLOR[1],
PLAYER_SIDE_BALL_COLOR[2]);
}
void move(player_t *player){
}
実装したのは自機キャラを初期化するplayerInit関数、自機キャラの状態を更新するplayerUpdate関数、自機キャラを実際に描画するplayerDraw関数の3つです。自機キャラはplayer_t構造体で表します。あとは、図形の色や配置など個人的に満足なパラメータをはじめに設定しています。ここでは、constをつけてパラメータを定義しました。
それでは自機キャラを表示させます。
##自機キャラの描画
これから作っていくシーンはゲームシーンにします。メニューシーンはしばらく放っておきます。弾幕シューティングが一通りできたらメニューを作っていきます。
メニューシーンはfpsとmenuという表示があり、ゲーム開始から1秒後以降にキーボードでsを押すとゲームシーンに遷移するようにしておきます。
#include "sceneMenu.h"
#include <GL/glut.h>
#include "../gameUtils.h"
static int count = 0;
static unsigned char param[SCENE_PARAMETER_MAX] = {1,2,3};
void sceneMenuUpdate(void (*changeSceneFunc)(enum eScene, unsigned char *, int)){
//update
count++;
if (count > 60 && keyGetState('s'))
changeSceneFunc(SCENE_GAME, param, 1);
glutPostRedisplay();
}
void sceneMenuDispaly(){
glClear(GL_COLOR_BUFFER_BIT);
fontBegin();
fontSetPosition(0.0, 100.0);
fontSetSize(FONT_DEFAULT_SIZE * 0.25);
fontSetWeight(1.0);
fontSetColor(0, 0, 255);
fontDraw("meun");
fontSetPosition(500.0, 400.0);
fontSetSize(FONT_DEFAULT_SIZE * 0.15);
fontSetWeight(1.0);
fontSetColor(255, 255, 255);
fontDraw("fps:%.1f", fpsGet());
fontEnd();
glutSwapBuffers();
}
おそらく長い間このメニューシーンでゲームがスタートします。
次にゲームシーンで自機キャラを描画します。
#include "sceneGame.h"
#include <GL/glut.h>
#include "../gameUtils.h"
#include "../../player.h"
static int count = 0;
static unsigned char param[SCENE_PARAMETER_MAX];
static player_t player;
void sceneGameInit(unsigned char *p){
for (int i = 0; i < SCENE_PARAMETER_MAX; i++)
param[i] = p[i];
playerInit(&player);
}
void sceneGameUpdate(void (*changeSceneFunc)(enum eScene, unsigned char *, int)){
//update
count++;
playerUpdate(&player);
glutPostRedisplay();
}
void sceneGameDispaly(){
glClear(GL_COLOR_BUFFER_BIT);
//font test
fontBegin();
fontSetPosition(0.0, 100.0);
fontSetSize(FONT_DEFAULT_SIZE * 0.5);
fontSetWeight(3.0);
fontSetColor(0, 0, 255);
fontDraw("game");
fontSetPosition(500.0, 400.0);
fontSetSize(FONT_DEFAULT_SIZE * 0.15);
fontSetWeight(1.0);
fontSetColor(255, 255, 255);
fontDraw("fps:%.1f", fpsGet());
fontEnd();
playerDraw(&player);
glutSwapBuffers();
}
まずはシーンの初期化関数の中でplayerInit関数を呼んで自機キャラを初期化します。その後、シーン更新関数の中で自機キャラ更新関数を呼び、シーン描画関数の中で実際に自機キャラを描画します。
それではこれまでの変更をMakefileにささっとかいて、コンパイル、ビルドして、実行します。
できましたー!
ようやくゲームっぽさが出ました。
#自機キャラの操作
それでは描画した自機キャラを動かしてみます。操作は上下左右キーを使うことにします。低速モードはvキーを押している間ずっとにします。東方projectの弾幕シューティングだと低速モードは左シフトキーなのですが、そこは少し変えてみようかと。
void move(player_t *player){
float x = 0.0f;
float y = 0.0f;
if (keyGetState('v'))
player->m_slow = 1;
if (!keyGetState('v') && player->m_slow)
player->m_slow = 0;
if (keyGetState(KEY_UP))
y -= PLAYER_SPEED;
if (keyGetState(KEY_DOWN))
y += PLAYER_SPEED;
if (keyGetState(KEY_LEFT))
x -= PLAYER_SPEED;
if (keyGetState(KEY_RIGHT))
x += PLAYER_SPEED;
if (x != 0.0f && y != 0.0f){
x /= (float)sqrt(2.0);
y /= (float)sqrt(2.0);
}
if (player->m_slow){
x /= 3.0f;
y /= 3.0f;
}
player->m_x += x;
player->m_y += y;
}
やっていることは簡単です。低速モードの判定、入力キーから自機の移動量の決定、自機の移動です。
上下と左右のキーが同時に押されていた場合は、斜め方向に移動してしまうため、定義した自機の移動量より大きくなってしまいます。直角二等辺三角形を思い浮かべてもらうとわかりますが、斜めに移動するということは、直角二等辺三角形の斜辺の長さだけ移動することになります。この斜辺の長さは、そのほかの辺の長さのルート2倍です。したがって移動量を合わせるために、斜め移動のときは移動量をルート2で割っていつでも同じ移動量を実現しています。
また、低速モードのときは、移動量を1/3にしています。
それではmakeして実行してみます。
#5日目まとめ
- 自機キャラの実装ができた
- 自機キャラの移動を実装した
今回追加したファイル
src/
+- ...
+- player.h
+- player.c
+- shape/
+- rect.h
+- rect.c
+- ball.h
+- ball.c
ようやくゲームっぽくなってきました。完成に向けて頑張ります。