0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

glutとC言語でシューティングゲーム4日目

Posted at

#これまでのあらすじ

  • キーボードからの入力を受け付ける仕組みができた
  • Makefileがまともになった

3日目はここです。

#4日目
弾幕シューティング開発4日目です。これまであまり弾幕シューティングに関係なさそうな部分の開発が続いてきました。今回でゲームの基盤が完成を目指します。

今回はFPS(flame per second)を制御します。前回までのプログラムだと、1秒間に実行されるループの回数が実行するPCの環境によって違います。そこで、どのようなPCを使っても1秒間に実行されるループの回数をほぼ一定の値に制御できるプログラムを目指します。といっても制限できるのは上限値であって、下限値は制御しません。(FPSの下限値制御ってできるのかな)今回使うFPSの値は60とします。おそらくゲームだと一般的な値だと思います。要するに、1秒間に60回フレームが更新されるゲームです。

FPS制御ができたら、ゲームの骨格は完成です。あとは、シーンを追加して登録するだけで実行してくれるような設計にしてあるからですね。そこで、今回の最後で今まで作成してきたファイル群を整理しようと思います。ただ、私は複数ファイルからなるプログラムの開発経験があまりないもので、経験者から見たら不自然な分け方だったり、違和感あるまとめ方になるかもしれません。そのようなときは、指摘していただけたら嬉しいです。

それでは実装していきます。

#今日の目標

  • FPS制御
  • 今あるファイル群の配置の見直し

#FPS制御
FPS制御の実装は、龍神録2プログラミングの館 7章. FPSの制御を行う を参考にしました。参考にしたのは、実際にFPSを計算する部分です。

120フレーム分の時刻をリストに保持し、そこから平均を算出します。
ポイントは、待機する時間を1つ前のフレームから計算しないこと。
[龍神録2プログラミングの館 7章. FPSの制御を行う]より引用

このプログラムでも、120フレーム分の時刻を保持してその保持したデータからFPSと待つべき時間を算出することにします。保持されているなかで一番古い時刻と現在の時刻との差と、その間に存在するべきフレーム数を計算するのに必要な時間とを比較し、FPSを60に保つために待機するべき時間を計算します。

そこで、時刻を取り扱う必要が出てきます。1秒間に60回フレームがなければいけないので、1フレーム当たり1000ms / 60 = 16.66...ms、1フレームにかける時間は約16.6ミリ秒となります。そのため、ミリ秒の制度の時刻を扱う必要があります。ここで、このゲームでFPSを計算するために必要な時刻は、私たちが生活で用いる時刻である必要はなく、ゲームを開始からの経過時間でもよいわけです。なので、まずはゲーム開始から経過した時間をミリ秒で取得する処理を追加します。

timeUtils.h
#ifndef ___HEADER_TIMEUTILS

#define ___HEADER_TIMEUTILS


void timeUtilsInit();
unsigned int timeUtilsGetMilliSeconds();


#endif
timeUtils.c
#include "timeUtils.h"

#include <time.h>
#include <sys/time.h>

static struct timeval start;

void timeUtilsInit(){
  gettimeofday(&start, NULL);
}

unsigned int timeUtilsGetMilliSeconds(){
  struct timeval tv;
  gettimeofday(&tv, NULL);
  time_t diffsec = difftime(tv.tv_sec, start.tv_sec);
  suseconds_t diffusec = tv.tv_usec - start.tv_usec;
  return (unsigned int)diffsec * 1000 + (unsigned int)(diffusec / 1000);
}

timeUtilsInit関数で基準となる時刻を設定し、timeUtilsGetMilliSeconds関数が基準として設定された時刻からの経過時間をミリ秒単位で返します。timeval構造体などの詳しい情報は省略します。timeUtilsInit関数はgame.cで定義されるgameInit関数内で呼び出します。

次に実際にFPSを制御する部分を実装します。

fps.h
#ifndef ___HEADER_FPS

#define ___HEADER_FPS


void fpsInit();
void fpsWait();
float fpsGet();


#endif
fps.c
#include "fps.h"

#include <unistd.h>
#include "font.h"
#include <GL/glut.h>
#include "timeUtils.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;
}

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;

  int calcTime = (int)(1000.f / 60.f * len);
  int realTime = timeUtilsGetMilliSeconds() - stockTime.m_d[stockTime.m_oldestPos];
  int wait = calcTime - realTime;
  if (wait < 0) wait = 0;
  return (unsigned int)wait;
}

void regist(){
  stockTime.m_d[stockTime.m_pos] = timeUtilsGetMilliSeconds();
  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;

  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;
  if (average == 0)
    return;
  fps = 1000.f / average;
}

提供する関数は、FPS制御の初期化を行うfpsInit関数、FPSが60になるように待機するfpsWait関数、現在の実際のFPSを返すfpsGet関数です。fpsInit関数は、gameInit関数内で呼び出します。fpsWait関数は1回のシーン計算が行われた後に毎回呼び出す必要があるのでgame.c内で定義されるgameLoop内で呼び出します。

実際に行われる処理ですが、120回の時刻を保持するためにstock_tというデータ構造を用います。実際に時刻を保持する配列、次のタイミングで時刻を格納する配列の場所を表す変数m_pos、保持されている時刻の数を表す変数m_len、一番古い時刻の場所を表す変数m_oldestPosを含んでいます。まあ、リスト作れって話ですけどね笑。時刻をstock_t型の変数に格納する処理をregist関数で、格納されている時刻からFPSを60にするために待つべき時間をミリ秒で返す処理をwaitTime関数で、格納されている時刻からFPSを計算する処理をupdateFps関数で、それぞれ行っています。

あとは、game.cの中に以下を追記します。

game.c
#include "timeUtils.h"
#include "fps.h"

//gameInitの中
timeUtilsInit();
fpsInit();

//gameLoop
void gameLoop(){
  sceneStackTop(&stk)->m_update(changeScene);
  fpsWait();
}

#FPSの確認
それでは、実際にFPS処理がなされているか確認します。メニューシーンで実際にFPSを表示させてみます。

sceneMenu.c
//display関数に追記
void sceneMenuDispaly(){
  //font test
  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();
}

あとはMakefileにtimeUtilsとfpsのコンパイルを追記して、ビルド、実行します。
day4.JPG
ちゃんと計算できているようです。

これで、ゲームの骨格が完成しました!!!!
あとはシーンを作ってゲームっぽく見せていきます。

#ここまでのファイルのまとめ
ゲームのシーンを作っていく前に、現状のファイルを整理します。現在、実行ファイルを生成するために以下のファイルが必要です。

Makefile 
main.c : メイン関数
define.h 
game.c game.h : ゲーム全体の管理
font.c font.h : 文字列描画
key.c key.h : キーボード入力監視
timeUtils.c timeUtils.h : 経過時間計算
fps.c fps.h : fps計算
scene.c scene.h : シーン管理

sceneMenu.c sceneMenu.h : メニューシーン
sceneGame.c sceneGame.h : ゲームシーン

大きく分けて、上の部分がゲームの骨格となる処理でこの後必要がない限り大きく変更しない部分、下の部分がゲームの内容となる部分でこの後作りこんでいく部分です。なので、まずはsceneというディレクトリを作成し、sceneなんとか.h/.cというファイルはsceneディレクトリに移します。それに伴って、includeのファイルパスが変わってきます。各シーンのヘッダーファイルをインクルードする必要があるのはgame.cです。しかし、game.cの中にインクルード命令がたくさん並ぶのは個人的に嫌なので、allScene.hというファイルを新たに用意し、そこに各シーンのヘッダーファイルのインクルード命令を追記していくことにします。そして、game.cではallScene.hをインクルードするだけで、各シーンのヘッダーファイルにアクセスできるようにしておきます。また、各シーンの実体を定義するsceneなんとか.cファイルでは、fps.h、font.h、key.hをほぼ毎回インクルードします。そこで、sceneディレクトリにgameUtils.hというファイルを作成し、そこにfps.h、font.h、key.hをすべてインクルードする命令を書いておきます。そうすることで、各シーンの.cファイルではgameUtils.hをインクルードするだけでよくなります。

allScene.h
#ifndef ___HEADER_ALLSCENE

#define ___HEADER_ALLSCENE


#include "./scene/menu/sceneMenu.h"
#include "./scene/game/sceneGame.h"


#endif
gameUtils.h
#ifndef ___HEADER_GAMEUTILS

#define ___HEADER_GAMEUTILS


#include "../fps.h"
#include "../font.h"
#include "../key.h"


#endif
game.c
//インクルード部分
#include "game.h"

#include <GL/glut.h>
#include "define.h"
#include "scene.h"
#include "allScene.h"
#include "key.h"
#include "fps.h"
#include "timeUtils.h"
ファイルの場所
src/
 +- Makefile 
 +- main.c : メイン関数
 +- define.h
 +- allScene.h 
 +- game.c
 +- game.h : ゲーム全体の管理
 +- font.c 
 +- font.h : 文字列描画
 +- key.c 
 +- key.h : キーボード入力監視
 +- timeUtils.c 
 +- timeUtils.h : 経過時間計算
 +- fps.c 
 +- fps.h : fps計算
 +- scene.c 
 +- scene.h : シーン管理
 +- scene/
     +- gameUtils.h
     +- menu/
     |   +- sceneMenu.c
     |   +- sceneMenu.h
     |
     +- Game/
     |   +- scenegame.c
     |   +- sceneGame.h 
     |
     +- ...

これで多少は管理しやすくなったと思います。

#4日目まとめ

  • ついにゲームの骨格が完成
  • 配置見直し

今回でついにゲームの骨格が完成しました!やったー!今回まででできたゲームの骨格は、シーンを別のものにするだけで様々なゲームになります。そのように設計したのだから当然ですけどね笑。なんとかオブジェクトを意識してやってこれました。まだまだ完成には程遠いですが、完成までがんばります。

#参照
龍神録2プログラミングの館
新・ゲームプログラミングの館

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?