ゲームを作りたい!

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

SDL2でRPGゲームを作る 〜第4回 プレイヤーは常に画面の中心〜

前回は猫に向きを与えて進行方向を向くようにした。いい感じになってきた気がするが何かが違う。
何が違うのか考えてみると、プレイヤーが画面のあちこちに移動してしまっているのに違和感を感じるのだ。
SFCドラクエなんかを思い出すと主人公は常に画面の中心にいたように思う。
よって、今回はプレイヤーは画面の中心に固定したまま移動を表現できるような方法を考えていく。

とりあえず悩んで考えついた方法は、
プレイヤーは画面の中心に固定しなくてはならないということだから後ろに表示されているマップを動かせば良い
といった方法だ。じゃあ、どうやったら、マップを動かすことができるだろう? と考えると、多分これも単純に考えればプレイヤーの位置からマップの描画範囲を計算してやれば良いはず。 あとは、マップ外の部分をなんらかの決まったマスで埋めてやれば、プレイヤーはそのままにマップだけを動かせそうだ。 イメージ図としてはこんな感じ。
f:id:K38:20181220003202j:plainf:id:K38:20181220003206j:plain

プレイヤーの位置からマップの描画範囲を計算する

マップ上のプレイヤーの位置から画面の描画範囲を計算する方法を考える。 マップのサイズが(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;
}

実行結果

実行結果はこんな感じ。猫が常に真ん中に表示されるようになった。
f:id:K38:20181220003520g:plain

終わりに

今回でだいぶRPG(ドラクエ)っぽい動きになってきたと思う。 しかしながら、まだ、マス単位でプレイヤーが動くのでカクカク動いている感じがある。 次回は、これをなめらかに移動できる方法を考えようと思う。