ゲームを作りたい!

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

SDL2でRPGゲームを作る 〜第10回 NPCを動かす〜

週にだいたい一回の投稿でやっとこさRPG作成もこれで十回目の投稿になる。作成スピードが遅いのでいつになったら遊べるレベルに到達できるかわからないが今度は二十回目を目標に作成していきたいと思う。
では、今回もRPGゲームを作っていこう。

前回はNPCの表示を実現した。プレイヤーを動作させた方法を真似しながらイベントファイルから読み込んだ情報を元にその場歩きさせることができた。今回は、その場歩きではなく前回イベントファイルに設定した移動可不可フラグを見てフラグが立っているNPCを動かしていこうと思う。

NPCを動かすためには

NPCを動かす方法についてだが、これまたプレイヤーの動作方法をそっくりまねれば動作させることができるはずである。しかしながら、プレイヤーと違ってキーボードからの入力がないので、その代わりになる仕組みを用意する必要がある。
今回は乱数を使って上下左右いずれかの情報がキー入力の代わりに発生するようにしてみた。
以下がその部分を実装したコードになる。

int npc_update(SDL_Renderer *renderer, int element) {

    srand((unsigned)time(NULL));
    int target = rand()%number_of_npc_image;
    int action = rand()%10;

    if (npc[element].npc_move == TRUE) {
        if (npc[element].npc.moving == TRUE) {
            npc[element].npc.pixel_x = npc[element].npc.pixel_x + npc[element].npc.velocity_x;
            npc[element].npc.pixel_y = npc[element].npc.pixel_y + npc[element].npc.velocity_y;

            if (npc[element].npc.pixel_x % GRID_SIZE == 0 && npc[element].npc.pixel_y % GRID_SIZE == 0) {
                npc[element].npc.moving = FALSE;
                npc[element].npc.map_x = npc[element].npc.pixel_x / GRID_SIZE;
                npc[element].npc.map_y = npc[element].npc.pixel_y / GRID_SIZE;
            }

        } else {
            if (target == element && action < 3) {
                npc_move(rand()%4, element);
            }
        }
    }
}

まず、この関数の対象はイベントファイルで移動可不可フラグが1になっているキャラクターになる。そして、 時間をシードとして上下左右(0〜3)いずれかの情報をnpc_move関数に渡す作りになっている。 また、動作するキャラクタと頻度を限定するためにメインループ一回ごとに動作するキャラクターと動作する確立を乱数で判定させている。 ちなみに、前回NPCの表示はマップ座標を使用していたが今回動かすにあたってpixel座標に変更した。
もうひとつ、ちなみにだが一般的に乱数の範囲をAからBまでと限定したいときは以下のように書くと良い。

rand()%(B-A+1)+A;

次に、動作部分を担当している関数を見てみる。

int npc_move(DIRECTION direction, int element) {

    if (frame == 0) {
        if (direction == UP){
            npc[element].npc.direction = UP;
            if (is_movable(npc[element].npc.map_x, npc[element].npc.map_y - 1) == 0) {
                npc[element].npc.velocity_x = 0;
                npc[element].npc.velocity_y = -speed;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == DOWN){
            npc[element].npc.direction = DOWN;
            if (is_movable(npc[element].npc.map_x, npc[element].npc.map_y + 1) == 0) {
                npc[element].npc.velocity_x = 0;
                npc[element].npc.velocity_y = speed;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == RIGHT){
            npc[element].npc.direction = RIGHT;
            if (is_movable(npc[element].npc.map_x + 1, npc[element].npc.map_y) == 0) {
                npc[element].npc.velocity_x = speed;
                npc[element].npc.velocity_y = 0;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == LEFT){
            npc[element].npc.direction = LEFT;
            if (is_movable(npc[element].npc.map_x - 1, npc[element].npc.map_y) == 0) {
                npc[element].npc.velocity_x = -speed;
                npc[element].npc.velocity_y = 0;
                npc[element].npc.moving = TRUE;
            }
        }
    }
    return 0;

}

作りはプレイヤーのものとほぼ同じ。違う所は入力が乱数から受け取っているところ。
ちなみに、最初のframe==0は、動作フレームを限定しないと動きすぎて気持ち悪かったので入れている。

その他変更点

細かいところだと色々いじっているが、NPCが動くことによって当たり判定を入れないとプレイヤーをすり抜ける現象が起きたのでプレイヤーとの当たり判定を追加している。だた、移動中の座標だと判定できないので タイミングによっては、まだ、すり抜け現象が起きる。もう少し、複雑に作ればそれも判定できると思うがめんどくさいので、もっと作りこんで余裕がでたときにでも改善していければと思う。

int is_movable(int x, int y) {

    int i;
    for(i = 0;i < number_of_npc_image;i++) {
        if (npc[i].npc.map_x == x && npc[i].npc.map_y == y) {
            return 1;
        }
    }

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

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

    if(player.map_x == x && player.map_y == y) {
        return 1;
    }

    return 0;
}

全コード表示

では、全コードを記載する。

move_npc.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 {
    CARACTER npc;
    char message[1024];
    SDL_Texture *npc_image;
    MOVING npc_move;
} NPC;

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


int clac_offset(int, int, int *, int *);
int load_image(SDL_Renderer *, SDL_Texture **, char *);
int player_animation(SDL_Renderer *);
int player_update(SDL_Renderer *, SDL_Event);
int player_move(SDL_Event);

int load_npc(SDL_Renderer *);
int npc_animation(SDL_Renderer *);
int npc_update(SDL_Renderer *renderer, int);
int npc_move(DIRECTION, int);

int load_map_image(SDL_Renderer *, SDL_Texture **);
int load_mapchip(SDL_Renderer *);
int load_move(SDL_Renderer *);
int load_map(char *);
int draw_map(SDL_Renderer *);
int is_movable(int, int);
int fade_out(SDL_Renderer *);

#endif

move_npc.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "RPGInC.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;
int ROW = 15;
int COL = 20;
int OUT_OF_MAP = 0;
char MAP_EVENT_NAME[256] = "field";

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

CARACTER player = {1, 1, 32, 32, 0, 0, 0, 0, DOWN, FALSE};
NPC npc[256] = {0};

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

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

    load_npc(renderer);

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

        npc_animation(renderer);

        player_animation(renderer);
        player_update(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);
    }

    for (i = 0;i < number_of_npc_image;i++) {
        SDL_DestroyTexture(npc[i].npc_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 player_animation(SDL_Renderer *renderer) {

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

    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 player_update(SDL_Renderer *renderer, SDL_Event e) {

    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;

            load_move(renderer);
            player_move(e);
        }

    } else {
        player_move(e);
    }

}

int player_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 load_npc(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    char npc_name[256];
    int map_x;
    int map_y;
    DIRECTION direction;
    MOVING moving;
    int max_step;
    char message[1024];

    char buf[256];
    char npc_path[256];
    int i = 0;
    int element_number = 0;

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

    for (i = 0;i < number_of_npc_image;i++) {
        npc[i].npc.map_x = 0;
        npc[i].npc.map_y = 0;
        npc[i].npc.pixel_x = 0;
        npc[i].npc.pixel_y = 0;
        npc[i].npc.direction = 0;
        npc[i].npc.moving = 0;
        SDL_DestroyTexture(npc[i].npc_image);
        sprintf(npc[i].message, "%s", '\0');
    }

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {

        if (strncmp(buf, "#", 1) != 0){
            if (strncmp(buf, "CHARA", 5) == 0) {
                sscanf(buf,
                   "%[^,],%[^,],%d,%d,%d,%d,%[^,]",
                       event, npc_name, &map_x, &map_y, &direction, &moving, message);

                sprintf(npc_path, "image/charachip/%s.bmp", npc_name);
                load_image(renderer, &npc[element_number].npc_image, npc_path);

                npc[element_number].npc.map_x = map_x;
                npc[element_number].npc.map_y = map_y;
                npc[element_number].npc.pixel_x = map_x * GRID_SIZE;
                npc[element_number].npc.pixel_y = map_y * GRID_SIZE;
                npc[element_number].npc.direction = direction;
                npc[element_number].npc_move = moving;

                sprintf(npc[element_number].message, "%s", message);

                element_number += 1;
            }
        }
    }

    number_of_npc_image = element_number;

    fclose(fp);

    return 0;
}

int npc_animation(SDL_Renderer *renderer) {

    int i;
    for(i = 0; number_of_npc_image >= i;i++) {

        int x = ((frame / animecycle) % 4) * 16;
        int y = npc[i].npc.direction * IMAGE_HEIGHT;

        SDL_Rect imageRect=(SDL_Rect){x/4, y/4, IMAGE_WIDTH/4, IMAGE_HEIGHT/4};
        SDL_Rect drawRect=(SDL_Rect){npc[i].npc.pixel_x - player.offset_x,
                                     npc[i].npc.pixel_y - player.offset_y,
                                     IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION};

        SDL_RenderCopy(renderer, npc[i].npc_image, &imageRect, &drawRect);

        if (npc[i].npc_move == TRUE && number_of_npc_image != 0) {
            npc_update(renderer, i);
        }
    }

    return 0;
}

int npc_update(SDL_Renderer *renderer, int element) {

    srand((unsigned)time(NULL));
    int target = rand()%number_of_npc_image;
    int action = rand()%10;

    if (npc[element].npc_move == TRUE) {
        if (npc[element].npc.moving == TRUE) {
            npc[element].npc.pixel_x = npc[element].npc.pixel_x + npc[element].npc.velocity_x;
            npc[element].npc.pixel_y = npc[element].npc.pixel_y + npc[element].npc.velocity_y;

            if (npc[element].npc.pixel_x % GRID_SIZE == 0 && npc[element].npc.pixel_y % GRID_SIZE == 0) {
                npc[element].npc.moving = FALSE;
                npc[element].npc.map_x = npc[element].npc.pixel_x / GRID_SIZE;
                npc[element].npc.map_y = npc[element].npc.pixel_y / GRID_SIZE;
            }

        } else {
            if (target == element && action < 3) {
                npc_move(rand()%4, element);
            }
        }
    }
}

int npc_move(DIRECTION direction, int element) {

    if (frame == 0) {
        if (direction == UP){
            npc[element].npc.direction = UP;
            if (is_movable(npc[element].npc.map_x, npc[element].npc.map_y - 1) == 0) {
                npc[element].npc.velocity_x = 0;
                npc[element].npc.velocity_y = -speed;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == DOWN){
            npc[element].npc.direction = DOWN;
            if (is_movable(npc[element].npc.map_x, npc[element].npc.map_y + 1) == 0) {
                npc[element].npc.velocity_x = 0;
                npc[element].npc.velocity_y = speed;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == RIGHT){
            npc[element].npc.direction = RIGHT;
            if (is_movable(npc[element].npc.map_x + 1, npc[element].npc.map_y) == 0) {
                npc[element].npc.velocity_x = speed;
                npc[element].npc.velocity_y = 0;
                npc[element].npc.moving = TRUE;
            }
        } else if (direction == LEFT){
            npc[element].npc.direction = LEFT;
            if (is_movable(npc[element].npc.map_x - 1, npc[element].npc.map_y) == 0) {
                npc[element].npc.velocity_x = -speed;
                npc[element].npc.velocity_y = 0;
                npc[element].npc.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_move(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    int event_point_x;
    int event_point_y;
    DIRECTION direction_of_penetration;
    char buf[256];
    char new_map_name[256];
    char map_path[256];
    int new_x;
    int new_y;
    int i = 0;

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

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {

        if (strncmp(buf, "#", 1) != 0){
            if (strncmp(buf, "MOVE", 4) == 0) {
                sscanf(buf,
                   "%[^,],%d,%d,%d,%[^,],%d,%d",
                       event, &event_point_x, &event_point_y, &direction_of_penetration, new_map_name, &new_x, &new_y);

                if (player.map_x == event_point_x && player.map_y == event_point_y) {
                    if (player.direction == direction_of_penetration) {
                        sprintf(MAP_EVENT_NAME, "%s", new_map_name);

                        sprintf(map_path, "data/%s.map", new_map_name);
                        load_map(map_path);

                        player.map_x = new_x;
                        player.map_y = new_y;
                        player.pixel_x = player.map_x * GRID_SIZE;
                        player.pixel_y = player.map_y * GRID_SIZE;

                        load_npc(renderer);

                        fade_out(renderer);
                    }
                }
            }
        }
    }

    fclose(fp);

    return 0;
}

int fade_out(SDL_Renderer *renderer) {

    SDL_Rect rectangle;
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    rectangle.x = 0;
    rectangle.y = 0;
    rectangle.w = SCREEN_WIDTH;
    rectangle.h = SCREEN_HEIGHT;

    int i = 200;
    int inverse_flg = 0;
    while(1) {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, i);
        SDL_RenderFillRect(renderer, &rectangle);
        SDL_RenderPresent(renderer);

        if (inverse_flg == 0 && i <= 255) {
            i = i + 5;
            SDL_Delay(80);
        }

        if (i == 255) {
           inverse_flg = 1;
        }

        if (inverse_flg == 1) {
            clac_offset(player.pixel_x, player.pixel_y, &player.offset_x, &player.offset_y);
            draw_map(renderer);
            player_animation(renderer);
            SDL_Delay(20);

            i = i - 5;

            if (i == 200) {
                break;
            }
        }
    }

    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 load_mapchip(SDL_Renderer *renderer) {

    FILE *fp;
    int x, y, z;
    char n[256];
    char path[256];
    char buf[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;fgets(buf, sizeof(buf), fp) != NULL;i++){
        sscanf(buf, "%d,%[^,],%d,%d", &x, n, &y, &z);
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].change_locate = z;

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

    }

    number_of_map_image = i - 1;

    fclose(fp);

    return 0;

}

int is_movable(int x, int y) {

    int i;
    for(i = 0;i < number_of_npc_image;i++) {
        if (npc[i].npc.map_x == x && npc[i].npc.map_y == y) {
            return 1;
        }
    }

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

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

    if(player.map_x == x && player.map_y == y) {
        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;
}

実行結果

実行結果はこんな感じ。NPCが動いた。
f:id:K38:20190216012600g:plain

終わりに

今回はNPCを動かしてみた。ここらで、いっちょ手の混んだフィールドでも作ってみるかなと思ったのだが 作りだしてすぐに今の実装だとマップチップが10個しか使えないことに気づいた。
読み込みとマップファイルのデータの持ち方がよろしくないのだ。
よって、次回からはマップを作るためのツールとマップチップを読み込むための手法も考えていこうと思う。 メッセージボックスとかも作っていきたいから並行して作っていければと思う。

SDL2でRPGゲームを作る 〜第9回 NPCの表示〜

前回は暗転処理を実現した。移動時にフェードアウト・インが起きることで違和感なくマップ間が移動できるようなった。今回は、いままでプレイヤー・キャラクターだけで、ぼっち感が漂っていたのでNPCの表示を考えてみようと思う。

NPCを表示するためには

NPCを表示する方法を考えてみようと思う。単純に考えればプレイヤーは表示できているのだから、その方法をまねればNPCも表示できるはずだ。ただ、プレイヤーの表示をそのまま利用して一体ずつ表示していたら管理がしづらくなってしまうので、マップのように簡単に追加・削除できるように作りたい。
そこで、NPCの表示は以前->マップ間移動の時に用意したイベントファイルに表示したいNPCを追加することで表示できるように作っていきたいと思う。

イベントファイル

イベントファイルには以下のように記載することにする。
field.evt

CHARA,white_cat,3,3,1,1,にゃーにゃー
CHARA,brown_cat,8,5,0,1,にゃーにゃーにゃー
CHARA,black_cat,2,8,2,1,にゃーにゃーにゃー
CHARA,marble_cat,10,10,0,1,にゃーにゃー
  • 1カラム目・・・イベント名
  • 2カラム目・・・キャラクター名
  • 3カラム目・・・マスX座標
  • 4カラム目・・・マスY座標
  • 5カラム目・・・向き
  • 6カラム目・・・移動可不可
  • 7カラム目・・・メッセージ

今回は、6,7カラム目は使わないが今後使うために記載しておく。
このイベントファイルから情報を読み取って表示できるように作っていく。

イベントファイルの読み込み

さっそく、イベントファイルを読み込む関数を作ってみた。

int load_npc(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    char npc_name[256];
    int map_x;
    int map_y;
    DIRECTION direction;
    MOVING moving;
    char message[1024];

    char buf[256];
    char npc_path[256];
    int i = 0;
    int element_number = 0;

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

    for (i = 0;i < number_of_npc_image;i++) {
        SDL_DestroyTexture(npc[i].npc_image);
    }

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {

        if (strncmp(buf, "#", 1) != 0){
            if (strncmp(buf, "CHARA", 5) == 0) {
                sscanf(buf,
                   "%[^,],%[^,],%d,%d,%d,%d,%[^,]",
                       event, npc_name, &map_x, &map_y, &direction, &moving, message);

                sprintf(npc_path, "image/charachip/%s.bmp", npc_name);
                load_image(renderer, &npc[element_number].npc_image, npc_path);

                npc[element_number].npc.map_x = map_x;
                npc[element_number].npc.map_y = map_y;
                npc[element_number].npc.direction = direction;
                npc[element_number].npc.moving = moving;

                sprintf(npc[element_number].message, "%s", message);

                element_number += 1;
            }
        }
    }

    number_of_npc_image = element_number;

    fclose(fp);

    return 0;
}

イベントファイルの読み込み方自体は、マップ間移動で取った方法と全く同じなので特筆する点はない。
今回、特筆する点はNPC用の構造体を用意したところである。

typedef struct {
    CARACTER npc;
    char message[1024];
    SDL_Texture *npc_image;
} NPC;

プレイヤー表示に使ったCARACTER構造体とnpc表示に使うためのテクスチャーとメッセージを持っている。

NPC npc[256] = {0};

これを、配列で定義してここにイベントファイルに書いたNPCを追加して使うことにした。

NPCのアニメーション

アニメーションもプレイヤーと同じような方法で行う。

int npc_animation(SDL_Renderer *renderer) {

    int i;
    for(i = 0; number_of_npc_image >= i;i++) {

        int x = ((frame / animecycle) % 4) * 16;
        int y = npc[i].npc.direction * IMAGE_HEIGHT;

        SDL_Rect imageRect=(SDL_Rect){x/4, y/4, IMAGE_WIDTH/4, IMAGE_HEIGHT/4};
        SDL_Rect drawRect=(SDL_Rect){(npc[i].npc.map_x * GRID_SIZE) - player.offset_x,
                                     (npc[i].npc.map_y * GRID_SIZE) - player.offset_y,
                                     IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION};

        SDL_RenderCopy(renderer, npc[i].npc_image, &imageRect, &drawRect);
    }


    return 0;
}

プレイヤーとの違いはイベントファイルに記載されているキャラクター分をこの関数で一気に表示できるように ループしていることくらいだ。
あと、原因が追い切れなかった(追う気が起きなかった)のだがテクスチャーを保持するとサイズが4分の1になってしまったので、とりあえず4で割って帳尻を合わせた。たぶん、マップチップのサイズに引っ張られている気がするが、 (キャラクターの画像はマップチップの4倍ある)まあ、表示できたから良しとしたい。

全コード表示

では、全コードを記載する。

display_npc.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 {
    CARACTER npc;
    char message[1024];
    SDL_Texture *npc_image;
} NPC;

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


int clac_offset(int, int, int *, int *);
int load_image(SDL_Renderer *, SDL_Texture **, char *);
int player_animation(SDL_Renderer *);
int player_update(SDL_Renderer *, SDL_Event);
int player_move(SDL_Event);

int load_npc(SDL_Renderer *);
int npc_animation(SDL_Renderer *);

int load_map_image(SDL_Renderer *, SDL_Texture **);
int load_mapchip(SDL_Renderer *);
int load_move(SDL_Renderer *);
int load_map(char *);
int draw_map(SDL_Renderer *);
int is_movable(int, int);
int fade_out(SDL_Renderer *);

#endif

display_npc.c

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "RPGInC.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;
int ROW = 15;
int COL = 20;
int OUT_OF_MAP = 0;
char MAP_EVENT_NAME[256] = "field";

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

CARACTER player = {1, 1, 32, 32, 0, 0, 0, 0, DOWN, FALSE};
NPC npc[256] = {0};

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

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

    load_npc(renderer);

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

        npc_animation(renderer);
        player_animation(renderer);
        player_update(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);
    }

    for (i = 0;i < number_of_npc_image;i++) {
        SDL_DestroyTexture(npc[i].npc_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 player_animation(SDL_Renderer *renderer) {

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

    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 player_update(SDL_Renderer *renderer, SDL_Event e) {

    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;

            load_move(renderer);
            player_move(e);
        }

    } else {
        player_move(e);
    }

}

int player_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 load_npc(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    char npc_name[256];
    int map_x;
    int map_y;
    DIRECTION direction;
    MOVING moving;
    char message[1024];

    char buf[256];
    char npc_path[256];
    int i = 0;
    int element_number = 0;

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

    for (i = 0;i < number_of_npc_image;i++) {
        SDL_DestroyTexture(npc[i].npc_image);
    }

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {

        if (strncmp(buf, "#", 1) != 0){
            if (strncmp(buf, "CHARA", 5) == 0) {
                sscanf(buf,
                   "%[^,],%[^,],%d,%d,%d,%d,%[^,]",
                       event, npc_name, &map_x, &map_y, &direction, &moving, message);

                sprintf(npc_path, "image/charachip/%s.bmp", npc_name);
                load_image(renderer, &npc[element_number].npc_image, npc_path);

                npc[element_number].npc.map_x = map_x;
                npc[element_number].npc.map_y = map_y;
                npc[element_number].npc.direction = direction;
                npc[element_number].npc.moving = moving;

                sprintf(npc[element_number].message, "%s", message);

                element_number += 1;
            }
        }
    }

    number_of_npc_image = element_number;

    fclose(fp);

    return 0;
}

int npc_animation(SDL_Renderer *renderer) {

    int i;
    for(i = 0; number_of_npc_image >= i;i++) {

        int x = ((frame / animecycle) % 4) * 16;
        int y = npc[i].npc.direction * IMAGE_HEIGHT;

        SDL_Rect imageRect=(SDL_Rect){x/4, y/4, IMAGE_WIDTH/4, IMAGE_HEIGHT/4};
        SDL_Rect drawRect=(SDL_Rect){(npc[i].npc.map_x * GRID_SIZE) - player.offset_x,
                                     (npc[i].npc.map_y * GRID_SIZE) - player.offset_y,
                                     IMAGE_WIDTH*MAGNIFICATION, IMAGE_HEIGHT*MAGNIFICATION};

        SDL_RenderCopy(renderer, npc[i].npc_image, &imageRect, &drawRect);
    }


    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_move(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    int event_point_x;
    int event_point_y;
    DIRECTION direction_of_penetration;
    char buf[256];
    char new_map_name[256];
    char map_path[256];
    int new_x;
    int new_y;
    int i = 0;

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

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {

        if (strncmp(buf, "#", 1) != 0){
            if (strncmp(buf, "MOVE", 4) == 0) {
                sscanf(buf,
                   "%[^,],%d,%d,%d,%[^,],%d,%d",
                       event, &event_point_x, &event_point_y, &direction_of_penetration, new_map_name, &new_x, &new_y);

                if (player.map_x == event_point_x && player.map_y == event_point_y) {
                    if (player.direction == direction_of_penetration) {
                        sprintf(MAP_EVENT_NAME, "%s", new_map_name);

                        sprintf(map_path, "data/%s.map", new_map_name);
                        load_map(map_path);

                        player.map_x = new_x;
                        player.map_y = new_y;
                        player.pixel_x = player.map_x * GRID_SIZE;
                        player.pixel_y = player.map_y * GRID_SIZE;

                        load_npc(renderer);

                        fade_out(renderer);
                    }
                }
            }
        }
    }

    fclose(fp);

    return 0;
}

int fade_out(SDL_Renderer *renderer) {

    SDL_Rect rectangle;
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    rectangle.x = 0;
    rectangle.y = 0;
    rectangle.w = SCREEN_WIDTH;
    rectangle.h = SCREEN_HEIGHT;

    int i = 200;
    int inverse_flg = 0;
    while(1) {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, i);
        SDL_RenderFillRect(renderer, &rectangle);
        SDL_RenderPresent(renderer);

        if (inverse_flg == 0 && i <= 255) {
            i = i + 5;
            SDL_Delay(80);
        }

        if (i == 255) {
           inverse_flg = 1;
        }

        if (inverse_flg == 1) {
            clac_offset(player.pixel_x, player.pixel_y, &player.offset_x, &player.offset_y);
            draw_map(renderer);
            player_animation(renderer);
            SDL_Delay(20);

            i = i - 5;

            if (i == 200) {
                break;
            }
        }
    }

    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 load_mapchip(SDL_Renderer *renderer) {

    FILE *fp;
    int x, y, z;
    char n[256];
    char path[256];
    char buf[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;fgets(buf, sizeof(buf), fp) != NULL;i++){
        sscanf(buf, "%d,%[^,],%d,%d", &x, n, &y, &z);
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].change_locate = z;

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

    }

    number_of_map_image = i - 1;

    fclose(fp);

    return 0;

}

int is_movable(int x, int y) {

    int i;
    for(i = 0;i < number_of_npc_image;i++) {
        if (npc[i].npc.map_x == x && npc[i].npc.map_y == y) {
            return 1;
        }
    }

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

実行結果

実行結果はこんな感じ。NPCが表示された。
f:id:K38:20190207002357g:plain

終わりに

今回はNPCを表示した。これで、ぼっちとはさよならだ。ただ、みんなその場で足踏みしてるだけではつまらないので、次回は、このNPCを動かす方法を考えてみたい。

SDL2でRPGゲームを作る 〜第8回 移動時の暗転処理〜

前回はマップ間移動を実現した。これで、マップファイルを凝ればかなりRPGっぽくできるようになったと思う。
しかしながら、移動であとひとつ実現したいと思っているのが移動時の暗転処理(フェードイン・アウト)。
ドラクエなんかだと移動時に一旦暗くなってから移動先に移る。前回までの移動処理では、パッとマップが 切り替わってしまうので少し違和感があった。
暗転処理。これを今回は実現させたい。

どうやって暗転させる?

まず、どうやって暗転処理実現するかだが、考えた結果、画面全体に透過度のある絵を上から被せて、それを 暗くしたり明るくしたりしたらそれっぽくなるのではないかという結論に至った。 ということで、この考えに基づいて実装してみようと思う。

暗転処理

暗転処理のために以下の関数を用意した。引数として、使っているレンダラー(絵を描くキャンパスみたいなもの)を受け取って、その上に、透過度のある画像を-> アルファブレンドしている。
作ってみて、だんだん暗くする(フェードアウト)は比較的簡単にできたのだが、だんだん明るくする(フェードイン)のは 結構悩んだ。フェードアウトは次々と透過度のある画像をブレンドしていけばいいのだが、フェードインは 明るくしなくてはならないので、常に元の画像に対して画像を重ねる必要があったからだ。 結果、メインのループとほぼ同じことをこの関数でもやっている。もっと、良い方法が思いついたら直そうと思うが 今はこれで良しとしたい。ちなみに、関数名はfade_outとなっているが、動作的にはアウトしてインしてるから 厳密に言うとfade_outは間違っているが、まあ、それも良しとしたいと思う。

int fade_out(SDL_Renderer *renderer) {

    SDL_Rect rectangle;
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    rectangle.x = 0;
    rectangle.y = 0;
    rectangle.w = SCREEN_WIDTH;
    rectangle.h = SCREEN_HEIGHT;

    int i = 200;
    int inverse_flg = 0;
    while(1) {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, i); 
        SDL_RenderFillRect(renderer, &rectangle);
        SDL_RenderPresent(renderer);

    if (inverse_flg == 0 && i <= 255) {
        i = i + 5;
            SDL_Delay(80);
    }

    if (i == 255) {
       inverse_flg = 1; 
    }

    if (inverse_flg == 1) {
            clac_offset(player.pixel_x, player.pixel_y, &player.offset_x, &player.offset_y);
            draw_map(renderer);
            character_animation(renderer);
            SDL_Delay(20);

        i = i - 5;

        if (i == 200) {
        break;
        }
    } 
    }

    return 0;

}

SDL2の関数を載せるのは久々になるが、今回、新しく登場したSDL2の関数は、
-> SDL_SetRenderDrawBlendMode
-> SDL_SetRenderDrawColor
-> SDL_RenderFillRect
の3つである。これらを使用して、暗転処理に使う透過画像を用意した。

では、全コードを記載する。

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 change_locate;
    SDL_Texture *map_image;
} MAPCHIP;


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

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

#endif

map_read.c

#include <stdio.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "RPGInC.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;
char MAP_EVENT_NAME[256] = "field";

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);
    character_update(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_Texture *cat_image = NULL;
    // load_image(renderer, &cat_image, "image/charachip/black_cat.bmp");
    load_image(renderer, &cat_image, "image/charachip/white_cat.bmp");

    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_update(SDL_Renderer *renderer, SDL_Event e) {

    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;

            load_event(renderer);
        character_move(e);
        }

    } else {
    character_move(e);
    }

}

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_event(SDL_Renderer *renderer) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    int event_point_x;
    int event_point_y;
    DIRECTION direction_of_penetration;
    char buf[256];
    char new_map_name[256];
    char map_path[256];
    int new_x;
    int new_y;
    int i = 0;

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

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {
        sscanf(buf,
           "%[^,],%d,%d,%d,%[^,],%d,%d",
               event, &event_point_x, &event_point_y, &direction_of_penetration, new_map_name, &new_x, &new_y);
        
    if (strcmp(event,"MOVE") == 0) {
            if (player.map_x == event_point_x && player.map_y == event_point_y) {
        if (player.direction == direction_of_penetration) {
                    sprintf(MAP_EVENT_NAME, "%s", new_map_name);

                    sprintf(map_path, "data/%s.map", new_map_name);
            load_map(map_path);

                    player.map_x = new_x;
                    player.map_y = new_y;
                    player.pixel_x = player.map_x * GRID_SIZE;
                    player.pixel_y = player.map_y * GRID_SIZE;

            fade_out(renderer);
        }
            }
    }
    }

    fclose(fp);

    return 0;
}

int fade_out(SDL_Renderer *renderer) {

    SDL_Rect rectangle;
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    rectangle.x = 0;
    rectangle.y = 0;
    rectangle.w = SCREEN_WIDTH;
    rectangle.h = SCREEN_HEIGHT;

    int i = 200;
    int inverse_flg = 0;
    while(1) {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, i); 
        SDL_RenderFillRect(renderer, &rectangle);
        SDL_RenderPresent(renderer);

    if (inverse_flg == 0 && i <= 255) {
        i = i + 5;
            SDL_Delay(80);
    }

    if (i == 255) {
       inverse_flg = 1; 
    }

    if (inverse_flg == 1) {
            clac_offset(player.pixel_x, player.pixel_y, &player.offset_x, &player.offset_y);
            draw_map(renderer);
            character_animation(renderer);
            SDL_Delay(20);

        i = i - 5;

        if (i == 200) {
        break;
        }
    } 
    }

    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];
    char buf[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;fgets(buf, sizeof(buf), fp) != NULL;i++){
        sscanf(buf, "%d,%[^,],%d,%d", &x, n, &y, &z);
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].change_locate = 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;
}

実行結果

実行結果はこんな感じ。それっぽく暗転できてると思う。
f:id:K38:20190130232014g:plain

終わりに

今回は暗転処理を実現した。これで移動に関してはとりあえず終了にしようかなと思う。
次回は、今までプレイヤーだけで寂しい感じだったのでNPCをどうやって表示するかを考えてみようかなと思う。

SDL2でRPGゲームを作る 〜第7回 マップ間移動〜

前回はマップを外部ファイルから読み込んで使うようにしてみた。 ファイルから読み込めるようになったことで、変更したいときにわざわざコンパイルしなおさなくても容易に変更ができるようになった。
今回は、マップ間移動ができるようにしていきたいと思う。今まで、ひとつのマップ上をウロウロしていたが マップ間移動を実装することで移動だけならばRPGっぽい感じにすることができると思う。

イベントファイルを用意する

まず、どうやってマップ間移動を実現するかだが、考えた結果イベントファイルなるものを用意して そこに記載されているマス座標(街とか洞窟とかそんなやつがある座標)に移動した時に指定したマップファイルを 読み込むことでマップが変わったように見せようと思う。

ということで、以下のようなイベントファイルを用意した。

field.evt

MOVE,5,5,0,tower,5,1
MOVE,5,5,1,tower,10,4
MOVE,5,5,2,tower,1,4
MOVE,5,5,3,tower,5,8

このイベントファイルはマップファイルと対になっていて、拡張子違いの同ファイル名としている。
各パラメータの意味は以下のようになっている。 -> マップファイルは前回の記事を参照

  • 1カラム目がどのような動作が実行されるか
    (今は移動を表す'MOVE'しか入れてないがのちのち何か足すかもということで動作を表すカラムを用意した)
  • 2カラム目、3カラム目はイベントが発生するXマス座標、Yマス座標
  • 4カラム目はどの向きでイベント座標に侵入したか(DOWN:0, LEFT:1, RIGHT:2, UP:3)
  • 5カラム目は移動先のフィールド名
  • 6カラム目、7カラム目は移動先のXマス座標、Yマス座標

ということで、このfield.evtの1行目を解読すると、

field.mapのx:5,y:5のマス座標に上から(下向きに)入って、tower.mapのx:5,y:1のマス座標に移動('MOVE’)する

といった意味になる。

これを、マス移動が完了するたびに毎回判定しながら移動することでマップ間移動ができるようになる。
また、イベントファイルにしたことによって、前回同様変更の際に再コンパイルが不要となるのでマップ作成が 多少なりとも捗るような作りだと思う。

イベントファイルの判定

イベントファイルを判定するための関数は以下を作成した。
先ほどのイベントファイルの説明をそのまま判定する関数である。マス移動ごとに常にイベントファイル内の イベントと内容が合致するかを判定し、合致した場合プレイヤーとマップを書き換える動きをする。

int load_event(void) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    int event_point_x;
    int event_point_y;
    DIRECTION direction_of_penetration;
    char buf[256];
    char new_map_name[256];
    char map_path[256];
    int new_x;
    int new_y;
    int i = 0;

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

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {
        sscanf(buf,
           "%[^,],%d,%d,%d,%[^,],%d,%d",
               event, &event_point_x, &event_point_y, &direction_of_penetration, new_map_name, &new_x, &new_y);
        
    if (strcmp(event,"MOVE") == 0) {
            if (player.map_x == event_point_x && player.map_y == event_point_y) {
        if (player.direction == direction_of_penetration) {
                    sprintf(MAP_EVENT_NAME, "%s", new_map_name);

                    sprintf(map_path, "data/%s.map", new_map_name);
            load_map(map_path);

                    player.map_x = new_x;
                    player.map_y = new_y;
                    player.pixel_x = player.map_x * GRID_SIZE;
                    player.pixel_y = player.map_y * GRID_SIZE;
        }
            }
    }
    }

    fclose(fp);

    return 0;
}

load_event関数以外では大きく変わったところは特に無い。
1点気をつけたい箇所としてはload_event関数を呼び出すのはマス移動完了時点なので

    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;

            load_event();
        character_move(e);
        }

    } else {
    character_move(e);
    }

character_animation関数のここに入ることくらいである。 では、全コードを記載する。

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 change_locate;
    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_event(void);
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 "RPGInC.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;
char MAP_EVENT_NAME[256] = "field";

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

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

    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;

            load_event();
        character_move(e);
        }

    } else {
    character_move(e);
    }

    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_event(void) {
    char event_path[256];

    sprintf(event_path, "data/%s.evt", MAP_EVENT_NAME);

    FILE *fp;
    char event[256];
    int event_point_x;
    int event_point_y;
    DIRECTION direction_of_penetration;
    char buf[256];
    char new_map_name[256];
    char map_path[256];
    int new_x;
    int new_y;
    int i = 0;

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

    for(i = 0;fgets(buf, sizeof(buf), fp) != NULL;i++) {
        sscanf(buf,
           "%[^,],%d,%d,%d,%[^,],%d,%d",
               event, &event_point_x, &event_point_y, &direction_of_penetration, new_map_name, &new_x, &new_y);
        
    if (strcmp(event,"MOVE") == 0) {
            if (player.map_x == event_point_x && player.map_y == event_point_y) {
        if (player.direction == direction_of_penetration) {
                    sprintf(MAP_EVENT_NAME, "%s", new_map_name);

                    sprintf(map_path, "data/%s.map", new_map_name);
            load_map(map_path);

                    player.map_x = new_x;
                    player.map_y = new_y;
                    player.pixel_x = player.map_x * GRID_SIZE;
                    player.pixel_y = player.map_y * GRID_SIZE;
        }
            }
    }
    }

    fclose(fp);

    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];
    char buf[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;fgets(buf, sizeof(buf), fp) != NULL;i++){
        sscanf(buf, "%d,%[^,],%d,%d", &x, n, &y, &z);
        mapchip[i].mapchip_id = x;
        strcpy(mapchip[i].mapchip_name, n);
        mapchip[i].movable = y;
        mapchip[i].change_locate = 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;
}

実行結果

実行結果はこんな感じ。マップ間移動を実現できた。
f:id:K38:20190126024302g:plain

終わりに

今回はマップ間移動を実現してみた。しかしながら、RPGのマップ間移動は移動時に 暗転(フェードイン・フェードアウト)が入って移動先に移る。 今回のマップ間移動はそんなこともなくスッとマップが切り替わるので少し違和感を感じてしまう。
次回は、マップ移動時の暗転に取り組みたい。さてさて、どうやったら実現できるかなぁ。

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らしさが増す気がするなぁ。

SDL2でRPGゲームを作る 〜第5回 滑らかな移動〜

年末年始と色々とバタバタして中々プログラムを組む時間&BLOGに記録する時間が取れなかったため久しぶりの更新になってしまった。今年どれだけゲーム作りに時間を避けるかわからないができる範囲で進めていこうと思う。

前回はプレイヤーを画面の中心に固定したまま移動を表現できるような方法を考えた。
いい感じに移動できるようになったが、マス単位で動作するので少しカクカクした感じがあった。
今回はこれを滑らかに移動する方法を考えていこうと思う。

滑らかに見せるには?

まず、そもそもマス単位で瞬間的に32ピクセル移動してしまうためにカクカクした動作に見えてしまっている事実がある。 一気に動くからカクカクして見える。
ということは、移動をマス単位ではなくピクセル単位で移動できるようになれば滑らかに見せれそうだ。

ピクセル単位での移動

今回の全コードを載せる。

#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 = 3;
int animecycle = 24;
int speed = 2;
int frame = 0;

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;

Caracter player = {1, 1, 32, 32, 0, 0, 0, 0, DOWN, FALSE};

int load_image(SDL_Renderer *, SDL_Texture **, char *);
int character_animation(SDL_Renderer *, SDL_Event);
int character_move(SDL_Event);
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, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                1, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 1,
                1, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 1,
                1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 2, 0, 0, 0, 0, 0, 0, 1,
                1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 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);
    }

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

    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){

    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");
    load_image(renderer, &map_image[2], "image/mapchip/mountain.bmp");

    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, 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 || map[y*COL+x] == 2){
        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;
}

前回から結構大きく変わって見えるが、これはマス単位で動作させていたものをピクセル単位に変更した結果である。全ての関数がピクセル単位で処理するように変更されている。
中でも今回の考えの肝になっているのがcharacter_move関数とcharacter_animation関数である。
プレイヤーに速度を与えて移動開始から終了してマスに収まるまで2ピクセルずつ画像を動かしていくようになっている。そして、マスに収まったタイミングでキー入力があるかを判定して入力があればそのまま移動するし なければ止まるみたいにしている。

実行結果

実行結果はこんな感じ。移動のカクカクした感じがなくなり滑らかに動くようになった。
f:id:K38:20190111003318g:plain
※なんかgifにしたら滑らか感がわからなくなった。実際は結構滑らかになっていると思う

終わりに

移動に関してはこれでとりあえず大丈夫!といった所まで作ったと思う。
次回はマップをもうちょっといい感じに取り込んで複数のマップが扱える土台を作っていきたいと思う。

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