ゲームを作りたい!

プログラミングをしているからには、いつかはゲームを作ってみたい。勉強したことを備忘録的に綴るBLOG。

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;
}

マップの草部分は以下の画像。
f:id:K38:20181129233704j:plain
海部分には以下の画像を用意した。
f:id:K38:20181129233746j:plain
マップのサイズは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にした際に画質がだいぶ落ちてしまったが雰囲気は感じられると思う)。
f:id:K38:20181129231824g:plain
無事にマップを表示できた。
しかしながら、当たり判定をしていないので、とうぜんながら海に入って画面外に出て行く動きができてしまう。

終わりに

今回はマップ表示の方法を考えてみた。とりあえず、マップの上を動けるようになったのでRPGゲーム作りの ひとまず一歩を踏み出したかな。 次回は当たり判定を導入して草地のみ動けるようにしようと思う。
あと、今回書いたコードだとマップを書くたびに画像をload_image関数で読み込んでいるためやたらとCPUを食っている。次回はその辺りも改善していきたい。