SDL2でRPGゲームを作る 〜第4回 プレイヤーは常に画面の中心〜
前回は猫に向きを与えて進行方向を向くようにした。いい感じになってきた気がするが何かが違う。
何が違うのか考えてみると、プレイヤーが画面のあちこちに移動してしまっているのに違和感を感じるのだ。
SFCのドラクエなんかを思い出すと主人公は常に画面の中心にいたように思う。
よって、今回はプレイヤーは画面の中心に固定したまま移動を表現できるような方法を考えていく。
とりあえず悩んで考えついた方法は、
プレイヤーは画面の中心に固定しなくてはならないということだから後ろに表示されているマップを動かせば良い
といった方法だ。じゃあ、どうやったら、マップを動かすことができるだろう?
と考えると、多分これも単純に考えればプレイヤーの位置からマップの描画範囲を計算してやれば良いはず。
あとは、マップ外の部分をなんらかの決まったマスで埋めてやれば、プレイヤーはそのままにマップだけを動かせそうだ。
イメージ図としてはこんな感じ。
プレイヤーの位置からマップの描画範囲を計算する
マップ上のプレイヤーの位置から画面の描画範囲を計算する方法を考える。 マップのサイズが(3200×3200)あるとすると、マップの中央にいるときのマップ上の座標は(1600,1600)になる。 それに対して、表示画面のサイズ(640×480)は決まっているので、表示画面上ではプレイヤーは(320,240)の位置にいることになる。 対応させると以下のようになる。
マップ上の座標 (1600,1600) 表示画面上の座標 (320,240)
プレイヤーが移動して表示画面上の座標が(0,0)になったとすると、
マップ上の座標 (1600 - 320,1600 - 240) 表示画面上の座標 (0,0)
となることから、マップ上の座標では(1280,1360)の位置にいることになる。
この(1280,1360)という座標はオフセットという値になる。
オフセットとは位置を基準点からの距離で表したものである。
この位置を基準点からの距離で考えるオフセットという考え方を用いると プレイヤーがどこにいても、マップ上の座標と表示画面上の座標の関係は以下のように表せる。
オフセット = プレイヤーのマップ上の座標 - (表示画面サイズ / 2) 表示画面上の座標 = プレイヤーのマップ上の座標 - オフセット
気をつけるとことしては、オフセットはプレイヤーの位置を基準点としているので 移動するたびにオフセットの再計算が必要なことだ。
では、この考えを元にコーディングしていく。
オフセットを実装する
まず、オフセットの計算部分。
int clac_offset(int x, int y, int *offset_x, int *offset_y) { *offset_x = (x * GRID_SIZE) - (SCREEN_WIDTH / 2); *offset_y = (y * GRID_SIZE) - (SCREEN_HEIGHT / 2); return 0; }
先ほどの考えをそのままコーディングしている。
続いては、オフセットを使用してのプレイヤーの描画部分。
int character_animation(SDL_Renderer *renderer, DIRECTION direction, int mx, int my) { SDL_Texture *cat_image = NULL; load_image(renderer, &cat_image, "image/charachip/black_cat.bmp"); int x = ((frame / animecycle) % 4) * 16; int y = direction * IMAGE_HEIGHT; SDL_Rect imageRect=(SDL_Rect){x, y, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){(mx * GRID_SIZE) - offset_x, (my * GRID_SIZE) - offset_y, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, cat_image, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } SDL_DestroyTexture(cat_image); return 0; }
drawRect
の計算部分で、これまた先ほどと同じく考えたものをそのままコーディングしている。
最後に、マップの描画部分。
int draw_map(SDL_Renderer *renderer){ SDL_Texture *map_image[NUMBER_OF_MAP_IMAGE]; load_image(renderer, &map_image[0], "image/mapchip/grass.bmp"); load_image(renderer, &map_image[1], "image/mapchip/water.bmp"); int x, y; int start_x = offset_x / GRID_SIZE - 1; int end_x = start_x + SCREEN_WIDTH / GRID_SIZE + 2; int start_y = offset_y / GRID_SIZE - 1; int end_y = start_y + SCREEN_HEIGHT/ GRID_SIZE + 2; for(y = start_y;y < end_y;y++){ for(x = start_x; x < end_x;x++){ SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){(x * GRID_SIZE) - offset_x, (y * GRID_SIZE) - offset_y, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; if ((x < 0) || (x > COL - 1) || (y < 0) || (y > ROW - 1)){ SDL_RenderCopy(renderer, map_image[1], &imageRect, &drawRect); } else { SDL_RenderCopy(renderer, map_image[map[y*COL+x]], &imageRect, &drawRect); } } } int i; for (i = 0;i < NUMBER_OF_MAP_IMAGE;i++) { SDL_DestroyTexture(map_image[i]); } return 0; }
ここは、前回と比べると大幅に変わっている。
マップの場合、プレイヤーの位置を中心としてマップのピクセル単位の座標とマス単位の座標を使うのでGRID_SIZE
を用意して
ピクセル座標をマス座標に置き換えている。
(start_x,start_y)が、マス単位での表示画面の左上、(end_x,end_y)がマス単位での表示画面の右下の座標になる。
また、プレイヤーがマップの一番左上に来た時などマップの範囲外を表示する必要が出てくる。その対応として、
SDL_RenderCopy(renderer, map_image[1], &imageRect, &drawRect);
で、範囲外はすべてwater.bmp
で表示する対応をとっている。
以下に、今回書いたコード全てを示す。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 2; const int ROW = 15; const int COL = 20; const int GRID_SIZE = 32; const int NUMBER_OF_MAP_IMAGE = 2; int animecycle = 60; int frame = 0; int offset_x = 0; int offset_y = 0; typedef enum {DOWN, LEFT, RIGHT, UP} DIRECTION; int load_image(SDL_Renderer *, SDL_Texture **, char *); int character_animation(SDL_Renderer *, DIRECTION, int, int); int draw_map(SDL_Renderer *); int is_movable(int, int); int clac_offset(int, int, int *, int *); int map[300] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; int main (int argc, char *argv[]) { SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "DRAW IMAGE TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN ); if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } int mx = 1; int my = 1; DIRECTION direction = DOWN; // main loop while (1) { clac_offset(mx, my, &offset_x, &offset_y); SDL_RenderClear(renderer); draw_map(renderer); character_animation(renderer, direction, mx, my); SDL_RenderPresent(renderer); // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ direction = UP; if (is_movable(mx, my - 1) == 0) { my = my - 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ direction = DOWN; if (is_movable(mx, my + 1) == 0) { my = my + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ direction = RIGHT; if (is_movable(mx + 1, my) == 0) { mx = mx + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ direction = LEFT; if (is_movable(mx - 1, my) == 0) { mx = mx - 1; } } } } IMG_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } int load_image(SDL_Renderer *renderer, SDL_Texture **image_texture, char *filename) { SDL_Surface *image = NULL; // 画像の読み込み image = IMG_Load(filename); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey(image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); *image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_FreeSurface(image); return 0; } int character_animation(SDL_Renderer *renderer, DIRECTION direction, int mx, int my) { SDL_Texture *cat_image = NULL; load_image(renderer, &cat_image, "image/charachip/black_cat.bmp"); int x = ((frame / animecycle) % 4) * 16; int y = direction * IMAGE_HEIGHT; SDL_Rect imageRect=(SDL_Rect){x, y, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){(mx * GRID_SIZE) - offset_x, (my * GRID_SIZE) - offset_y, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, cat_image, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } SDL_DestroyTexture(cat_image); return 0; } int draw_map(SDL_Renderer *renderer){ SDL_Texture *map_image[NUMBER_OF_MAP_IMAGE]; load_image(renderer, &map_image[0], "image/mapchip/grass.bmp"); load_image(renderer, &map_image[1], "image/mapchip/water.bmp"); int x, y; int start_x = offset_x / GRID_SIZE - 1; int end_x = start_x + SCREEN_WIDTH / GRID_SIZE + 2; int start_y = offset_y / GRID_SIZE - 1; int end_y = start_y + SCREEN_HEIGHT/ GRID_SIZE + 2; for(y = start_y;y < end_y;y++){ for(x = start_x; x < end_x;x++){ SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){(x * GRID_SIZE) - offset_x, (y * GRID_SIZE) - offset_y, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; if ((x < 0) || (x > COL - 1) || (y < 0) || (y > ROW - 1)){ SDL_RenderCopy(renderer, map_image[1], &imageRect, &drawRect); } else { SDL_RenderCopy(renderer, map_image[map[y*COL+x]], &imageRect, &drawRect); } } } int i; for (i = 0;i < NUMBER_OF_MAP_IMAGE;i++) { SDL_DestroyTexture(map_image[i]); } return 0; } int is_movable(int x, int y) { if ( x < 0 || x > COL - 1 || y < 0 || y > ROW - 1) { return 1; } if (map[y*COL+x] == 1 ){ return 1; } return 0; } int clac_offset(int x, int y, int *offset_x, int *offset_y) { *offset_x = (x * GRID_SIZE) - (SCREEN_WIDTH / 2); *offset_y = (y * GRID_SIZE) - (SCREEN_HEIGHT / 2); return 0; }
実行結果
実行結果はこんな感じ。猫が常に真ん中に表示されるようになった。
終わりに
今回でだいぶRPG(ドラクエ)っぽい動きになってきたと思う。 しかしながら、まだ、マス単位でプレイヤーが動くのでカクカク動いている感じがある。 次回は、これをなめらかに移動できる方法を考えようと思う。
SDL2でRPGゲームを作る 〜第3回 キャラクターに進行方向を向かせる〜
前回はマップとの当たり判定を実装して猫を草地に閉じ込めることができた。しかしながら、どの方向に動いても 常に右向きの状態で移動していた。今回はキャラクターに進行方向を向かせる方法を考えたい。
表示画像の変更
まず、今まで使用していた画像は以下。
このように、16×64の右方向を向いている画像だけを使っていた。
今回は、上下左右どの方向も向けるように以下のように64×64の画像を用意した。
この画像を使用して進行方向を向かせていく。
まず、前回から変更した部分のコードを示す。前回から大きくは変わっていないが
キャラクターに向きの概念を導入した。
typedef enum {DOWN, LEFT, RIGHT, UP} DIRECTION; DIRECTION direction = DOWN; // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ direction = UP; if (is_movable(mx, my - 1) == 0) { my = my - 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ direction = DOWN; if (is_movable(mx, my + 1) == 0) { my = my + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ direction = RIGHT; if (is_movable(mx + 1, my) == 0) { mx = mx + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ direction = LEFT; if (is_movable(mx - 1, my) == 0) { mx = mx - 1; } } }
int character_animation(SDL_Renderer *renderer, DIRECTION direction, int mx, int my) { SDL_Texture *cat_image = NULL; load_image(renderer, &cat_image, "image/charachip/black_cat.bmp"); int x = ((frame / animecycle) % 4) * 16; int y = direction * IMAGE_HEIGHT; SDL_Rect imageRect=(SDL_Rect){x, y, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx * GRID_SIZE, my * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, cat_image, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } SDL_DestroyTexture(cat_image); return 0; }
キー入力時に上下左右の状態を取得し、character_animationで画像を表示するときに画像のY座標の位置を
int y = direction * IMAGE_HEIGHT;
で求めている。
こうすることで、進行方向の画像を入力に合わせて表示することができた。
以下に、全コードをのせる。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 2; const int ROW = 15; const int COL = 20; const int GRID_SIZE = 32; int animecycle = 60; int frame = 0; typedef enum {DOWN, LEFT, RIGHT, UP} DIRECTION; int load_image(SDL_Renderer *, SDL_Texture **, char *); int character_animation(SDL_Renderer *, DIRECTION, int, int); int draw_map(SDL_Renderer *); int is_movable(int, int); int map[300] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; int main (int argc, char *argv[]) { SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "DRAW IMAGE TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIG if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } int mx = 10; int my = 8; DIRECTION direction = DOWN; // main loop while (1) { SDL_RenderClear(renderer); draw_map(renderer); character_animation(renderer, direction, mx, my); SDL_RenderPresent(renderer); // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ direction = UP; if (is_movable(mx, my - 1) == 0) { my = my - 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ direction = DOWN; if (is_movable(mx, my + 1) == 0) { my = my + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ direction = RIGHT; if (is_movable(mx + 1, my) == 0) { mx = mx + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ direction = LEFT; if (is_movable(mx - 1, my) == 0) { mx = mx - 1; } } } } IMG_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } int load_image(SDL_Renderer *renderer, SDL_Texture **image_texture, char *filename) { SDL_Surface *image = NULL; // 画像の読み込み image = IMG_Load(filename); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey(image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); *image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_FreeSurface(image); return 0; } int character_animation(SDL_Renderer *renderer, DIRECTION direction, int mx, int my) { SDL_Texture *cat_image = NULL; load_image(renderer, &cat_image, "image/charachip/black_cat.bmp"); int x = ((frame / animecycle) % 4) * 16; int y = direction * IMAGE_HEIGHT; SDL_Rect imageRect=(SDL_Rect){x, y, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx * GRID_SIZE, my * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, cat_image, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } SDL_DestroyTexture(cat_image); return 0; } int draw_map(SDL_Renderer *renderer){ SDL_Texture *grass_image = NULL; SDL_Texture *water_image = NULL; load_image(renderer, &water_image, "image/mapchip/water.bmp"); load_image(renderer, &grass_image, "image/mapchip/grass.bmp"); int x, y; for(y = 0;y < ROW;y++){ for(x = 0; x < COL;x++){ SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){x * GRID_SIZE, y * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION} if (map[y*COL+x] == 1) { SDL_RenderCopy(renderer, water_image, &imageRect, &drawRect); } else if (map[y*COL+x] == 0) { SDL_RenderCopy(renderer, grass_image, &imageRect, &drawRect); } } } SDL_DestroyTexture(grass_image); SDL_DestroyTexture(water_image); return 0; } int is_movable(int x, int y) { if ( x < 0 || x > COL - 1 || y < 0 || y > ROW - 1) { return 1; } if (map[y*COL+x] == 1 ){ return 1; } return 0; }
実行結果
今回実行した結果は以下。
このように猫に進行方向を向かせることができた。
終わりに
猫が進行方向を向いたことで移動はなんとなくいい感じになってきた。
しかしながら、ドラクエなどを思い返すと主人公は常に画面の真ん中に表示されてたように思う。
次回は、そのような表示をさせる方法について考えてみたいと思う。
SDL2でRPGゲームを作る 〜第2回 マップとの当たり判定を入れる〜
前回はマップを表示する方法を考えた。しかし、マップとの当たり判定がなかったためにキャラクターが画面外に
出て行けてしまっていた。今回は当たり判定を実装して、前回表示した草地にキャラクターを閉じ込めたいと思う。
あと、前回とりあえずマップを表示できていたがCPUをやたら食うといった現象が出ていた。
それにも、対処してみたのでコードは前回からだいぶ変わっている。しかし、今回のコードのほうが前回に比べて見やすくなっていると思う。
当たり判定を入れる
まずは、どうすればマップとの当たり判定ができるのかを考える。単純に考えればキャラクターが移動しようとしたマスが進んで良いマスかダメなマスかを判定して、移動して良い時だけその方向に進むようにできればいいということになる。ということは、キー入力があった時に進行方向のマスが進めるかを判断したうえで移動させてやれば良い。
この考え方に基づいて実装したコードが以下の部分になる。
SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ if (is_movable(mx, my - 1) == 0) { my = my - 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ if (is_movable(mx, my + 1) == 0) { my = my + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ if (is_movable(mx + 1, my) == 0) { mx = mx + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ if (is_movable(mx - 1, my) == 0) { mx = mx - 1; } } }
int is_movable(int x, int y) { if ( x < 0 || x > COL - 1 || y < 0 || y > ROW - 1) { return 1; } if (map[y*COL+x] == 1 ){ return 1; } return 0; }
進行方向のマスが進めるかどうかをis_movable関数で判断したうえで 移動を開始するようにする。
is_movable関数は、受け取ったマスの情報を元に、進めるのであれば「0」進めないのであれば「1」を返す。
以下にマップとの当たり判定を入れたコードを示す。
ちなみに、前回と大きく変更したのはdraw_map関数のforループの中で画像を読み込んでいたのを先に読み込んでからforループを回すようにした箇所。その他もちょこちょこいじっている。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 2; const int ROW = 15; const int COL = 20; const int GRID_SIZE = 32; int animecycle = 60; int frame = 0; int load_image(SDL_Renderer *, SDL_Texture **, char *); int character_animation(SDL_Renderer *, int, int); int draw_map(SDL_Renderer *); int is_movable(int, int); int map[300] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; int main (int argc, char *argv[]) { SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "DRAW IMAGE TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN ); if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } int mx = 10; int my = 8; // main loop while (1) { SDL_RenderClear(renderer); draw_map(renderer); character_animation(renderer, mx, my); SDL_RenderPresent(renderer); // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ if (is_movable(mx, my - 1) == 0) { my = my - 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ if (is_movable(mx, my + 1) == 0) { my = my + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ if (is_movable(mx + 1, my) == 0) { mx = mx + 1; } } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ if (is_movable(mx - 1, my) == 0) { mx = mx - 1; } } } } IMG_Quit(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } int load_image(SDL_Renderer *renderer, SDL_Texture **image_texture, char *filename) { SDL_Surface *image = NULL; // 画像の読み込み image = IMG_Load(filename); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey(image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); *image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_FreeSurface(image); return 0; } int character_animation(SDL_Renderer *renderer, int mx, int my) { SDL_Texture *cat_image = NULL; load_image(renderer, &cat_image, "image/charachip/walkcat.bmp"); int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx * GRID_SIZE, my * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, cat_image, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } SDL_DestroyTexture(cat_image); return 0; } int draw_map(SDL_Renderer *renderer){ SDL_Texture *grass_image = NULL; SDL_Texture *water_image = NULL; load_image(renderer, &water_image, "image/mapchip/water.bmp"); load_image(renderer, &grass_image, "image/mapchip/grass.bmp"); int x, y; for(y = 0;y < ROW;y++){ for(x = 0; x < COL;x++){ SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){x * GRID_SIZE, y * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION} if (map[y*COL+x] == 1) { SDL_RenderCopy(renderer, water_image, &imageRect, &drawRect); } else if (map[y*COL+x] == 0) { SDL_RenderCopy(renderer, grass_image, &imageRect, &drawRect); } } } SDL_DestroyTexture(grass_image); SDL_DestroyTexture(water_image); return 0; } int is_movable(int x, int y) { if ( x < 0 || x > COL - 1 || y < 0 || y > ROW - 1) { return 1; } if (map[y*COL+x] == 1 ){ return 1; } return 0; }
実行結果
今回実行した結果は以下。
わかりづらいと思うが猫を海に入らないようにするようにできた。
終わりに
今回はマップとの当たり判定の方法を考えてみた。 他に進めない場所を作るにはis_movable関数に条件を追加していけば良いのでマップ上の動作はこれでOKかな。 ただ、動かしてて気になるのが猫の向きがずっと右向きなこと。 次はとりあえず、猫に進行方向を向かせようと思う。
SDL2でRPGゲームを作る 〜第1回 マップを表示する〜
前回の投稿から間が開いてしまったが、その間にどんなゲームを作りたいかをつらつらと考えてみた。
自分がどんなゲームをしていたかを思い返すと、ドラクエやFFといったRPGをよくやっていたように思う。
クロノ・トリガーなんかは何周したかわからないくらいプレイした。あの、細部まで作りこまれたゲームは、やるたびに新しい発見があって全てを遊び尽くせた気がしていない。今プレイしなおしても新しい発見があるように思う。
・・・といったように、自分の中でゲームといったらRPGが王道のようだ。よって、RPGゲームを作っていこうと思う。
とは言っても、いきなりあんなすごいゲームが作れる気は全くしない。一歩ずつ実現方法を考えていこうと思う。
マップを表示する
さて、作ると言ってもなにから手を付けていけば良いのやらなのでなんとなくできそうなところから実装していく。
とりあえず、前回まででキャラクターの表示はできているので
(-> 画像の表示,
-> アニメーションの表示)、
今回はマップを表示する方法を考えようと思う。先に、今回実装したコードを載せておく。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 2; const int ROW = 15; const int COL = 20; const int GRID_SIZE = 32; int animecycle = 5; int frame = 0; typedef enum {TRUE, FALSE} TRANSPARENT; int load_image(SDL_Renderer *, SDL_Surface **, SDL_Texture **, char *, TRANSPARENT); int character_animation(SDL_Renderer *, SDL_Surface **, SDL_Texture **, int, int); int draw_map(SDL_Renderer *, SDL_Surface **, SDL_Texture **); int map[300] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; int main (int argc, char *argv[]) { SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; SDL_Surface *image = NULL; SDL_Texture *image_texture = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "DRAW IMAGE TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN ); if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); int iw,ih; SDL_QueryTexture(image_texture, NULL, NULL, &iw, &ih); // 猫の初期位置 int mx = 10; int my = 8; // main loop while (1) { SDL_RenderClear(renderer); draw_map(renderer, &image, &image_texture); character_animation(renderer, &image, &image_texture, mx, my); SDL_RenderPresent(renderer); // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ my = my - 1; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ my = my + 1; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ mx = mx + 1; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ mx = mx - 1; } } } IMG_Quit(); SDL_FreeSurface(image); SDL_DestroyTexture(image_texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } int load_image(SDL_Renderer *renderer, SDL_Surface **image, SDL_Texture **image_texture, char *filename, TRANSPARENT status) int flags=IMG_INIT_JPG|IMG_INIT_PNG; int initted=IMG_Init(flags); if(initted&flags != flags) { printf("IMG_Init: JPGとPNGの読み込みの初期化に失敗した!\n"); printf("IMG_Init: %s\n", IMG_GetError()); } // 画像の読み込み *image = IMG_Load(filename); if(!*image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 if (status == TRUE) { SDL_SetColorKey( *image, SDL_TRUE, SDL_MapRGB((*image)->format, 255, 0, 255)); } *image_texture = SDL_CreateTextureFromSurface(renderer, *image); return 0; } int character_animation(SDL_Renderer *renderer, SDL_Surface **image, SDL_Texture **image_texture, int mx, int my) { TRANSPARENT status = TRUE; load_image(renderer, &*image, &*image_texture, "image/charachip/walkcat.png", status); int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x, 0, IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx * GRID_SIZE, my * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderCopy(renderer, *image_texture, &imageRect, &drawRect); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } return 0; } int draw_map(SDL_Renderer *renderer, SDL_Surface **image, SDL_Texture **image_texture){ int x, y; TRANSPARENT status = FALSE; for(y = 0;y < ROW;y++){ for(x = 0; x < COL;x++){ if (map[y*COL+x] == 1) { load_image(renderer, &*image, &*image_texture, "image/mapchip/water.png", status); } else if (map[y*COL+x] == 0) { load_image(renderer, &*image, &*image_texture, "image/mapchip/grass.png", status); } SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){x * GRID_SIZE, y * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION} SDL_RenderCopy(renderer, *image_texture, &imageRect, &drawRect); } } return 0; }
マップの草部分は以下の画像。
海部分には以下の画像を用意した。
マップのサイズは15×20(ROW:列、COL:行)で一次元配列で表現している。二次元配列を使っても良いのだが、一次元配列のほうが個人的に
扱いやすいのでそのようにした。二次元配列を一次元配列で扱うときは、
二次元配列 b[H][W] の要素 b[y][x] は,1次元配列 c[WH] の要素 c[yW + x] に対応するので
二次元配列 map[15][20] の要素 map[1, 1]は、1次元配列 map[15*20] の要素 map[y*20 + x] に対応する
のようにしてやればよい。(数字を当てはめると納得できると思う)
const int ROW = 15; const int COL = 20; int map[300] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
配列mapを、じーっと見てみるとなんとなくマップっぽく見えると思う。
とりあえず、草地と海だけなので0と1だけでマップを構成している。山とか岩とか森とか砂漠とか表示したいときは違う数値を
あてがってやれば良いはず。
const int GRID_SIZE = 32; int draw_map(SDL_Renderer *renderer, SDL_Surface **image, SDL_Texture **image_texture){ int x, y; TRANSPARENT status = FALSE; for(y = 0;y < ROW;y++){ for(x = 0; x < COL;x++){ if (map[y*COL+x] == 1) { load_image(renderer, &*image, &*image_texture, "image/mapchip/water.png", status); } else if (map[y*COL+x] == 0) { load_image(renderer, &*image, &*image_texture, "image/mapchip/grass.png", status); } SDL_Rect imageRect=(SDL_Rect){0, 0, IMAGE_WIDTH, IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){x * GRID_SIZE, y * GRID_SIZE, IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION} SDL_RenderCopy(renderer, *image_texture, &imageRect, &drawRect); } } return 0; }
今回一番重要なのがこのdraw_map関数。配列mapを見ながら草地と海を表示していくようになっている。
GRID_SIZEはマップ1マスのサイズを表していて16×16のサイズのマップチップを2倍にして使ってるので32が固定値で入っている。
drawRectに入れる座標は配列マップの位置×GRID_SIZEになっている。
配列マップにしたがってタイルを敷いてるようなイメージを持つとわかりやすいと思う。
実行結果
今回実行した結果は以下(gifにした際に画質がだいぶ落ちてしまったが雰囲気は感じられると思う)。
無事にマップを表示できた。
しかしながら、当たり判定をしていないので、とうぜんながら海に入って画面外に出て行く動きができてしまう。
終わりに
今回はマップ表示の方法を考えてみた。とりあえず、マップの上を動けるようになったのでRPGゲーム作りの
ひとまず一歩を踏み出したかな。 次回は当たり判定を導入して草地のみ動けるようにしようと思う。
あと、今回書いたコードだとマップを書くたびに画像をload_image関数で読み込んでいるためやたらとCPUを食っている。次回はその辺りも改善していきたい。
SDL2でマウス入力を検知する
今回はSDL2を使用してマウス入力の検知をする。 キーボードに次いでマウスからの入力ができればゲームの操作性が色々と考えられるようになって来るから楽しくなってくるよね。 では、早速マウス入力の検知はどのようにすればできるの調べていこう。
マウスイベントを検知する
まず、マウスイベントを検知するためには次の関数を使用するようだ。
-> SDL_Point
-> SDL_MouseButtonEvent
詳細は、リンク先を参照してもらうことにして、ざっくり説明すると
SDL_Pointを使用するとウィンドウ上のマウスの座標を得ることができて、
SDL_MouseButtonEventを使用することで右クリック左クリック等の識別ができる。
実にさらっとマウス入力の検知ができるようなので、前回のキーボート入力の検知のソース(-> ここを参照)をちょっと書き換えてマウス入力の検知を行った。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 4; typedef enum {right, left} CONTROL; int main (int argc, char *argv[]) { const int IMAGE_WIDTH_SENTER = IMAGE_WIDTH * MAGNIFICATION / 2; const int IMAGE_HEIGHT_SENTER = IMAGE_HEIGHT * MAGNIFICATION / 2; SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; SDL_Surface *image = NULL; SDL_Texture *image_texture = NULL; // ウィンドウの位置 SDL_Point window_position = { SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED }; // ウィンドウのサイズ SDL_Point window_size = {SCREEN_WIDTH, SCREEN_HEIGHT}; // マウスの座標 SDL_Point mouse_position = {IMAGE_WIDTH_SENTER, IMAGE_WIDTH_SENTER}; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "KEY EVENT TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN ); if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } // 画像の読み込み image = IMG_Load("walkcat.bmp"); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey( image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); int iw,ih; SDL_QueryTexture(image_texture, NULL, NULL, &iw, &ih); int animecycle = 60; int frame = 0; CONTROL status = right; // main loop while (1) { int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x,0,IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mouse_position.x - (IMAGE_WIDTH_SENTER), mouse_position.y - (IMAGE_HEIGHT_SENTER), IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderClear(renderer); if (status == right) { SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); } else if (status == left) { SDL_RenderCopyEx(renderer, image_texture, &imageRect, &drawRect,0 , NULL ,SDL_FLIP_HORIZONTAL); } SDL_RenderPresent(renderer); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_RIGHT){ // マウスのウィンドウ上の座標を得る SDL_GetMouseState(&mouse_position.x, &mouse_position.y); status = right; } else if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT){ SDL_GetMouseState(&mouse_position.x, &mouse_position.y); status = left; } } } IMG_Quit(); SDL_FreeSurface(image); SDL_DestroyTexture(image_texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }
このコードを実行すると、クリックした位置に画像が移動する。
また、右左どちらのクリックを検知しているかわかりやすくするために、
左クリックだと画像が左向き、右クリックだと画像が右向きになるようにしてみた。
マウス検知のために追加した部分
今回のマウス検知で新たに追加されたのは以下の部分。
SDL_Pointでウィンドウの位置やサイズを設定して、あとはSDL_MouseButtonEventを使用してクリックした箇所の座標を拾っている。
// ウィンドウの位置 SDL_Point window_position = { SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED }; // ウィンドウのサイズ SDL_Point window_size = {SCREEN_WIDTH, SCREEN_HEIGHT}; // マウスの座標 SDL_Point mouse_position = {IMAGE_WIDTH_SENTER, IMAGE_WIDTH_SENTER};
// event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_RIGHT){ // マウスのウィンドウ上の座標を得る SDL_GetMouseState(&mouse_position.x, &mouse_position.y); status = right; } else if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT){ SDL_GetMouseState(&mouse_position.x, &mouse_position.y); status = left; } }
その他の追加した部分
以下の部分では画像の貼り付け先の左上のx座標・y座標を指定してるが、マウスから取得した座標そのままだと
クリックした位置の右下に画像が出力されるのでクリックした位置が画像の中央になるように位置合わせをしている。
SDL_Rect drawRect=(SDL_Rect){mouse_position.x - (IMAGE_WIDTH_SENTER), mouse_position.y - (IMAGE_HEIGHT_SENTER), IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION};
以下の部分では今まで使ったことのない新たな関数を使用している。
これは、左クリックの時にキャラクターに左を向かせるために画像を反転させるために使用している。
詳細は以下のリンクで確認できる。
-> SDL_RenderCopyEx
-> SDL_RendererFlip
SDL_RenderClear(renderer); if (status == right) { SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); } else if (status == left) { SDL_RenderCopyEx(renderer, image_texture, &imageRect, &drawRect, 0, NULL ,SDL_FLIP_HORIZONTAL); }
実行結果
こんな感じでクリックに合わせて画像が移動する。
終わりに
これで、だいたいこれからゲームを作るにあたって基本的な所は調べたかなと思う。
あとは、どんなゲームを作るかによって色々と調べることが変わるだろうからどんなゲーム作るか少し考えてみようかな。とりあえず、今回はこんな感じで終わります。それではまた次回。
SDL2でキーボード入力を検知する
今回はSDL2を使用してキーボード入力の検知をする。 キーボードからの入力ができるようになるとゲームっぽい何かが作れそうな気がしてくるよね。 では、早速キーボード入力の検知はどのようにすればできるの調べていこう。
キーイベントを検知する
-> ここ(キーボートイベントタイプ)や
-> ここ(キーコード)にイベントの種類が色々まとめてある。
また、イベントを検知するには-> この関数(SDL_PollEvent)を使用するようだ。
早速コードを書いてみた。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 4; int main (int argc, char *argv[]) { SDL_Window* window = NULL; SDL_Renderer *renderer = NULL; SDL_Surface *image = NULL; SDL_Texture *image_texture = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "KEY EVENT TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN ); if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } // 画像の読み込み image = IMG_Load("walkcat.bmp"); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey( image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); int iw,ih; SDL_QueryTexture(image_texture, NULL, NULL, &iw, &ih); int animecycle = 60; int frame = 0; int mx = 0; int my = 0; // main loop while (1) { int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x,0,IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx,my,IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderClear(renderer); SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); SDL_RenderPresent(renderer); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ mx = mx + IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ mx = mx - IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ my = my - IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ my = my + IMAGE_WIDTH*MAGNIFICATION; } } } IMG_Quit(); SDL_FreeSurface(image); SDL_DestroyTexture(image_texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }
前回のアニメーションのコード(-> ここを参照)に キーイベントを加えてキャラクターの位置を変えられるようにしてみた。前回のコードとの違いは以下になる。
int animecycle = 60; int frame = 0; int mx = 0; int my = 0; // main loop while (1) { int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x,0,IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){mx,my,IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderClear(renderer); SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); SDL_RenderPresent(renderer); if (frame <= animecycle * 4) { frame += 1; } else{ frame = 0; } // event handling SDL_Event e; if ( SDL_PollEvent(&e) ) { if (e.type == SDL_QUIT){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){ break; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){ mx = mx + IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){ mx = mx - IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){ my = my - IMAGE_WIDTH*MAGNIFICATION; } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){ my = my + IMAGE_WIDTH*MAGNIFICATION; } } }
目新しくなったのは、whileループでアニメーションを表示しキーイベントを受け付けるようにしたところである。
今回追加したコードだとEscキーで処理を終了し矢印ボタンで表示されているキャラクターの位置が変化するようになっている。
キャラクターの位置を変えるのは単純に画像の大きさ分、x座標とy座標の位置を足したり引いたりしているだけ。
特に範囲を限定しているわけではないのでウィンドウ外にもキャラクターは移動できてしまうけど、
やりたかったのはキーボードからの検知だから、まぁOKってことにしよう。
実行結果
こんな感じでキャラクターの位置がキー入力に合わせて変化する。
終わりに
SDL2を使うことで結構簡単にキーイベント入力の検知ができた。
次回はマウスからの入力を検知してみようかな。
そこまでできれば、とりあえずゲームを作っていく最低限は調べた感じになるかなぁ。
SDL2でアニメーションを表示する
今回はSDL2を使用してアニメーションを表示していこうと思う。
基本的には-> ここの記事のコードをベースにしている。
アニメーションさせる画像の準備
今回アニメーションに使う画像は以下である。
16×16の画像が4枚横につながっていて16×64の画像になっている。
これを、4分割に切り取って順番に表示させることでアニメーションになる。
ソースコードと画像ファイルの階層は以下のようになっている。
sdl2_test/ |-animation.c |-walkcat.bmp
アニメーションさせる
今回は新たなSDL2の関数は使用していないので関数へのリンクは無し。
ただし、アニメーションさせるために導入した式と使用したSDL2の関数の説明を
次の項目で説明してみようと思う。
ちなみに、SDL_Init
とSDL_CreateWindow
はIf文の入れ子にしないで同じ深さになるようにしてみた。
#include <stdio.h> #include <SDL2/SDL.h> #include <SDL2/SDL_image.h> const int SCREEN_WIDTH = 640; const int SCREEN_HEIGHT = 480; const int IMAGE_WIDTH = 16; const int IMAGE_HEIGHT = 16; const int MAGNIFICATION = 4; int main (int argc, char *argv[]) { SDL_Window* window = NULL; SDL_Renderer *renderer = NULL; SDL_Surface *image = NULL; SDL_Texture *image_texture = NULL; //Initialize SDL if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() ); return 1; } window = SDL_CreateWindow( "DRAW IMAGE TEST", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIG if( window == NULL ) { printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() ); return 1; } else { renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); } // 画像の読み込み image = IMG_Load("walkcat.bmp"); if(!image) { printf("IMG_Load: %s\n", IMG_GetError()); return 1; } // 透過色の設定 SDL_SetColorKey( image, SDL_TRUE, SDL_MapRGB(image->format, 255, 0, 255)); image_texture = SDL_CreateTextureFromSurface(renderer, image); SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); int iw,ih; SDL_QueryTexture(image_texture, NULL, NULL, &iw, &ih); int animecycle = 60; int frame = 0; // main loop while (frame < 3000) { int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x,0,IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){300,220,IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderClear(renderer); SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); SDL_RenderPresent(renderer); frame += 1; } IMG_Quit(); SDL_FreeSurface(image); SDL_DestroyTexture(image_texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }
アニメーション部分の説明
アニメーションは、読み込んだ画像を次々と切り替えることで実現することができる。
これは、今回で言うと4分割した画像をなんらかのタイミングで切り替えて表示することができれば
アニメーションになるということだ。
その、画像の分割とタイミングを決めているのがこの部分。
int animecycle = 60; int frame = 0; // main loop while (frame < 3000) { int x = ((frame / animecycle) % 4) * 16; SDL_Rect imageRect=(SDL_Rect){x,0,IMAGE_WIDTH,IMAGE_HEIGHT}; SDL_Rect drawRect=(SDL_Rect){300,220,IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION}; SDL_RenderClear(renderer); SDL_RenderCopy(renderer, image_texture, &imageRect, &drawRect); SDL_RenderPresent(renderer); frame += 1; }
imageRect
とdrawRect
は、SDL_RenderCopy
に渡すSDL_Rect
構造体になる。
※SDL_Rect
構造体とは、左上を基点とした長方形を定義する構造体のこと。
imageRect
はコピー元のSDL_Rect
で、drawRect
はコピー先のSDL_Rect
だ。
SDL_Rect
構造体は、
int x 長方形の左上のX座標 int y 長方形の左上のY座標 int w 長方形の幅 int h 長方形の高さ
で構成されている。
よって、コピー元の画像を4分割してコピー先に渡すためには
x座標の位置を変えて画像を切り取っていけば良い。
今回使用している画像は一枚16×16の画像になるのでx座標の位置を
0, 16, 32, 48と変化させれば良いことになる。
x座標の位置を変化させるために使用している式がこの式。
int x = ((frame / animecycle) % 4) * 16;
(frame / animecycle) % 4
の部分で、0〜3までの数値を算出している。
そこに16をかけることでx座標の位置がでる。
また、この式を使うとframeがanimecycle分増加することで次の画像へと移り変わるので
アニメーションの間隔を自分で設定することができる。
数値の切り替わりは以下のようになっている。
frame=0 ( 0 / 60) % 4 = 0 frame=60 ( 60 / 60) % 4 = 1 frame=120 (120 / 60) % 4 = 2 frame=180 (180 / 60) % 4 = 3 frame=240 (240 / 60) % 4 = 0 frame=300 (300 / 60) % 4 = 1 frame=360 (360 / 60) % 4 = 2 frame=420 (420 / 60) % 4 = 3 以下同様に続く
以上が、アニメーションの説明になる。
もうひとつ説明して起きたいのがこの部分。
SDL_Rect drawRect=(SDL_Rect){300,220,IMAGE_WIDTH*MAGNIFICATION,IMAGE_HEIGHT*MAGNIFICATION};
ここで定義しているMAGNIFICATION
は画像の倍率。
というのも、SDL_RenderCopy
はコピー先で指定された幅と高さに画像を自動で拡大縮小してくれる。
MAGNIFICATION
を定義することで、好きな倍率に設定できるようにした。
今回はMAGNIFICATION
の部分を4にしているので、実際の画像より4倍大きく拡張して表示を行っている。
実行結果
実行結果はこんな感じ。ちゃんと、アニメーションになっている。
終わりに
今回はSDL2を使用してアニメーションを表示してみた。
この部分をうまく関数化すれば色々使いまわせるようになるんだろうなぁと思ったり。
次回は、SDL2を使ったキーボード入力の検知の方法を調べようと思う。
どうでもいいんだけど説明するっていうのは難しい。
頑張ってまとめてみたけど、後で見返した時に頭に???が浮かびそう。
文章力も鍛えてかないと記事書くのがコード書くより時間かかってしまうなぁ。