ゲームを作りたい!

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

SDL2でRPGゲームを作る 〜第6回 マップの読み込み〜

前回はプレイヤーをピクセル単位で表示させることで滑らかな動きができるようにした。
今回は、マップをどうにかコードにベタ打ちではなく外部から読み込むなどして容易に作成変更修正ができるようにしたい。

外部ファイルでマップを定義する

とりあえず、コードからマップデータを切り離すために以下のように外部ファイルにマップデータを定義してみた。

field.map

20 15
1
11111111111111111111
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000222220000001
10000000211120000001
10000000211120000001
10000000222220000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
11111111111111111111

1行目がマップの行列の長さ
2行目がマップ外で使用するマップチップ
それ以降がマップデータになっている

これをコードから呼び出してマップを生成するようにすることで、 この定義ファイルを変更すればマップの内容を変更できるようにする。

次に、前回まではマップチップの読み込みもコードにベタ打ちだったので以下のようにマップチップを定義するファイルを作成し、 追加があれば、このファイルに足していくだけでOKな状態にする。
mapchip.dat

0,grass,0,0
1,water,1,0
2,mountain,1,0
3,town,0,0

1列目がマップチップID
2列目がマップチップ名
3列目が移動可不可
4列目がマップチップを透過するかどうか

4列目に関しては現在全てのマップチップはロードするときに透過処理が入るようになっているのでいらない気もするが 一応いれておいた。ちなみに、今回この値は使わなかった。。。

ソースコードの分割

今回、外部ファイルを読み込む仕様にするにあたってソースコードの分割を行った。 今まで一つのファイルに全てのコードを記載していたが、以下のような構成に分割した。

|-map_read.c
|-map_read.h
|-Makefile
  |-data
  | |-mapchip.dat
  | |-field.map
  |-image
    |-mapchip
    | |-grass.bmp
    | |-water.bmp
    | |-mountain.bmp
    |-charachip
      |-black_cat.bmp

ファイル読み込み

外部ファイルを読み込むために大幅にコードの修正を行ったが、今回の肝になってくる関数が以下の2つになる

int load_map(char *map_name) {
    FILE *fp;
    int map_num;

    fp = fopen(map_name, "r");
    if (fp == NULL) {
        printf("file open error. %d\n", __LINE__);
        return 1;
    }

    fscanf(fp, "%d%d", &COL, &ROW);
    fscanf(fp, "%d", &OUT_OF_MAP);

    int i = 0;
    while((map_num = fgetc(fp)) != EOF){
        if(map_num != 0x0d){
            if(map_num != 0x0a){
                map_array[i] = map_num - 48;
                i++;
            }
       }
    }

    return 0;
}
int mapchip_load(SDL_Renderer *renderer) {

    FILE *fp;
    int x, y, z;
    char n[256];
    char path[256];
    int i = 0;

    fp = fopen("data/mapchip.dat", "r");
    if (fp == NULL) {
        printf("file open error. %d\n", __LINE__);
        return 1;
    }

    for(i = 0;(fscanf(fp, "%d,%[^,],%d,%d", &x, n, &y, &z)) != EOF;i++){
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].transmission = z;

    sprintf(path, "image/mapchip/%s.bmp", mapchip[i].mapchip_name);
    load_image(renderer, &mapchip[i].map_image, path);

    }

    fclose(fp);

    return 0;
    
}

int load_map(char *)は、その名前の通りマップを読み込むための関数になる。

load_map("data/field.map");

のように呼び出すことでマップを読み込むことができる。
ちなみに、-48しているのはファイルから読み込んだ値はchar型なのだが それを、int型配列に数値として入れたいがために-48をしている。 fgetcで文字を読み込むと戻り値がint型の符号なし文字になるので-48すると数値が表現できる。
0x0d 0x0aをはじいてるのは、改行コードなしのデータを一次元配列に入れるためである。

int mapchip_load(SDL_Rendrer *)は、これまたその名前の通りマップチップを読み込むための関数になる。

mapchip_load(renderer);

のように呼び出すことでマップチップを読み込むことができる。
ここは、fscanfで読み込んだ値を構造体にセットするだけなので特に変なことはしていない。

長ったらしくなるが以下に全コードを記載する。

map_read.h

#ifdef _RPGINC
#else

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>

typedef enum {DOWN, LEFT, RIGHT, UP} DIRECTION;
typedef enum {FALSE, TRUE} MOVING;

typedef struct {
    int map_x;
    int map_y;
    int pixel_x;
    int pixel_y;
    int offset_x;
    int offset_y;
    int velocity_x;
    int velocity_y;
    DIRECTION direction;
    MOVING moving;
} CARACTER;

typedef struct {
    int mapchip_id;
    char mapchip_name[256];
    int movable;
    int transmission;
    SDL_Texture *map_image;
} MAPCHIP;


int read_file(char *);
int id_max(void);

int load_image(SDL_Renderer *, SDL_Texture **, char *);
int character_animation(SDL_Renderer *, SDL_Event);
int character_move(SDL_Event);

int load_map_image(SDL_Renderer *, SDL_Texture **);
int mapchip_load(SDL_Renderer *);
int load_map(char *);
int draw_map(SDL_Renderer *);
int is_movable(int, int);
int clac_offset(int, int, int *, int *);

#endif

map_read.c

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "map_read.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 GRID_SIZE = 32;
const int NUMBER_OF_MAP_IMAGE = 5;
int ROW = 15;
int COL = 20;
int OUT_OF_MAP = 0;

int animecycle = 24;
int speed = 2;
int frame = 0;

CARACTER player = {1, 1, 32, 32, 0, 0, 0, 0, DOWN, FALSE};
MAPCHIP mapchip[256] = {0};

int map_array[65536] = {0};


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

    mapchip_load(renderer);
    load_map("data/field.map");

    // main loop
    while (1) {
        SDL_Event e;

        clac_offset(player.pixel_x, player.pixel_y, &player.offset_x, &player.offset_y);

        SDL_RenderClear(renderer);
        draw_map(renderer);
        character_animation(renderer, e);
        SDL_RenderPresent(renderer);

        // event handling
        if ( SDL_PollEvent(&e) ) {
            if (e.type == SDL_QUIT){
                break;
            } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE){
                break;
        }
    }

    }

    IMG_Quit();
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);

    int i;
    for (i = 0;i < NUMBER_OF_MAP_IMAGE;i++) {
        SDL_DestroyTexture(mapchip[i].map_image);
    }

    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, SDL_Event e) {

    SDL_Texture *cat_image = NULL;
    load_image(renderer, &cat_image, "image/charachip/black_cat.bmp");

    if (player.moving == TRUE) {
        player.pixel_x = player.pixel_x + player.velocity_x;
        player.pixel_y = player.pixel_y + player.velocity_y;

        if (player.pixel_x % GRID_SIZE == 0 && player.pixel_y % GRID_SIZE == 0){
            player.moving = FALSE;
            player.map_x = player.pixel_x / GRID_SIZE;
            player.map_y = player.pixel_y / GRID_SIZE;

        character_move(e);
        }

    } else {
    character_move(e);
    }

    int x = ((frame / animecycle) % 4) * 16;    
    int y = player.direction * IMAGE_HEIGHT;

    SDL_Rect imageRect=(SDL_Rect){x, y, IMAGE_WIDTH, IMAGE_HEIGHT};      
    SDL_Rect drawRect=(SDL_Rect){player.pixel_x - player.offset_x, player.pixel_y - player.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 character_move(SDL_Event e) {

    if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_UP){
        player.direction = UP;
        if (is_movable(player.map_x, player.map_y - 1) == 0) {
            player.velocity_x = 0;
            player.velocity_y = -speed;
            player.moving = TRUE;
        }
    } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_DOWN){
        player.direction = DOWN;
        if (is_movable(player.map_x, player.map_y + 1) == 0) {
            player.velocity_x = 0;
            player.velocity_y = speed;
            player.moving = TRUE;
        }
    } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RIGHT){
        player.direction = RIGHT;
        if (is_movable(player.map_x + 1, player.map_y) == 0) {
            player.velocity_x = speed;
            player.velocity_y = 0;
            player.moving = TRUE;
        }
    } else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_LEFT){
        player.direction = LEFT;
        if (is_movable(player.map_x - 1, player.map_y) == 0) {
            player.velocity_x = -speed;
            player.velocity_y = 0;
            player.moving = TRUE;
        }
    }
    
    return 0;

}

int draw_map(SDL_Renderer *renderer){

    int x, y;
    int start_x = player.offset_x / GRID_SIZE - 1;
    int end_x = start_x + SCREEN_WIDTH / GRID_SIZE + 2;
    int start_y = player.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) - player.offset_x,
                                 (y * GRID_SIZE) - player.offset_y,
                      IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION};

            if ((x < 0) || (x > COL - 1) || (y < 0) || (y > ROW - 1)){
                SDL_RenderCopy(renderer, mapchip[OUT_OF_MAP].map_image, &imageRect, &drawRect);
        } else {
                SDL_RenderCopy(renderer, mapchip[map_array[y*COL+x]].map_image, &imageRect, &drawRect);
            }

        }
    }

    return 0;
}


int load_map(char *map_name) {
    FILE *fp;
    int map_num;

    fp = fopen(map_name, "r");
    if (fp == NULL) {
        printf("file open error. %d\n", __LINE__);
        return 1;
    }

    fscanf(fp, "%d%d", &COL, &ROW);
    fscanf(fp, "%d", &OUT_OF_MAP);

    int i = 0;
    while((map_num = fgetc(fp)) != EOF){
        if(map_num != 0x0d){
            if(map_num != 0x0a){
                map_array[i] = map_num - 48;
                i++;
            }
       }
    }

    return 0;
}

int mapchip_load(SDL_Renderer *renderer) {

    FILE *fp;
    int x, y, z;
    char n[256];
    char path[256];
    int i = 0;

    fp = fopen("data/mapchip.dat", "r");
    if (fp == NULL) {
        printf("file open error. %d\n", __LINE__);
        return 1;
    }

    for(i = 0;(fscanf(fp, "%d,%[^,],%d,%d", &x, n, &y, &z)) != EOF;i++){
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].transmission = z;

    sprintf(path, "image/mapchip/%s.bmp", mapchip[i].mapchip_name);
    load_image(renderer, &mapchip[i].map_image, path);

    }

    fclose(fp);

    return 0;
    
}

int is_movable(int x, int y) {

    if ( x < 0 || x > COL - 1 || y  < 0 || y > ROW - 1) {
        return 1;
    }

    if(mapchip[map_array[y*COL+x]].movable == 1){
        return 1;
    }

    return 0;
}

int clac_offset(int x, int y, int *offset_x, int *offset_y) {
    *offset_x = x - (SCREEN_WIDTH / 2);
    *offset_y = y - (SCREEN_HEIGHT / 2);

    return 0;
}

全体を通して大きく変わった部分は、やはり外部ファイルを読み込んでその値を使って処理を行う部分になる。
マップチップについては構造体に情報を保持して使う方法を取っているし、マップについては必要なタイミングで ファイルから読み込んで使うような形になっている。
本当はもっと機能に絞ってソースコードの分割をするとグローバル変数をがしがし書き換えることなく もっといい感じになるような気もするが、とりあえず動くのでこれで良しとしておく。
※のちのち、どんどん書き換えていく気がするので今のとこはこれでOKにする

コンパイル

ヘッダファイルが増えたのでこんな感じでコンパイルする。

gcc -g -o map_read map_read.c map_read.h `sdl2-config --cflags --libs` -lSDL2_image

実行結果

実行結果はこんな感じ。前回と動作はなんら変わらないけどマップは追加変更しやすくなったと思う。 f:id:K38:20190118021219g:plain

終わりに

今回はマップを外部ファイルから読み込んで使うようにしてみた。
次回は、マップ間の移動をできるようにしていきたい。 マップ間移動ができると一気にRPGらしさが増す気がするなぁ。