关于RPG(2D)游戏框架的一些设想

废话

摘要:本文主要讲述了作者在游戏制作中对整体RPG游戏框架的一些思考及其实现,或包含其中的一些细节实现。

关键词:C语言,EGE图形库,RPG

前言:最近看了B站上的一些关于使用EGE编写游戏的实例,其中一个模仿仙剑三的游戏吸引了我的注意(链接),想着自己也想去实现个大概,便有了以下的内容。

注:文中的代码可能有不完整,或是多余的部分(因为是从现有代码截取的),这部分请读者自行忽略

何为RPG

角色扮演游戏(Role-playing game),简称为RPG,是游戏类型的一种。在游戏中,玩家负责扮演这个角色在一个写实或虚构世界中活动。

玩家负责扮演一个或多个角色,并在一个结构化规则下通过一些行动令所扮演的角色发展。玩家在这个过程中的成功与失败取决于一个规则或行动方针的形式系统(Formal system)。——百度百科

首先关注的一词,便是写实或虚拟世界,因此,便要考虑到游戏中对于该世界的载体(或者是存储方式)。

其次,活动。我们必然要考虑到游戏的输入必须能够控制游戏中某一实体的变量操控。另外,为了精确定义活动范围,我们必须引入坐标系的概念,这就要考虑如何建立坐标系的问题。

第三,扮演角色。和上点类似,同样是 玩家的输入能够控制某一特定或者多个变量。

第四,结构化规则。通俗易懂,便是流程,玩家可在一系列流程中拥有交互式体验。

当对整个RPG有了一个大概的了解后,便可以着手分块考虑需求的实现问题。

模块的实现

世界的载体&坐标系概念

世界应当是由多个场景构成的,正如我们生活中,我们不可能同时存在于美国与中国。当我们进入一个房间,那么房间对于我们应当是一个场景,而房间应当隶属于世界;当我们在公园,那么公园对于我们应当是一个场景,而公园同样隶属于世界。

正如我们的现实生活中一样,我们拥有标准度量衡(m)等概念,因此,世界应当是具有同一的度量衡的(正如地区单位的不同,如:米,英尺,造成的不便)。

那么如何选定这一度量衡呢?

不妨夸张思考现实中的一些实例,我们可以以原子的直径作为单位长度丈量(显得过于精细)

同样的,我们可以选定一个2*2cm的小方块作为单位面积(似乎合理些了)……

这样思考,我们便可以大胆设想,以区块(chunk)来定义这个世界(或者场景)

那么这个时候又会有一个很死循环的问题:我们的chunk该要求多大呢?

这个问题其实很好回答:这取决于你对这个世界精细程度的要求。

你往往可以将这个世界的区块定义的十分巨大,以至于当你和另外一个人相距十万八千里时,仍然能够proudly说:看,我们天涯若比邻!

其实很多游戏都有区块这一概念,比如我的世界(Minecraft)

可以看到其中清晰地描述了一些chunk的信息。

为了更好地描述chunk中有什么,我们可以给chunk加上类型描述。

我们便有了如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
#define CHUNK_SIZE 25

typedef enum EntityType{//实体类型枚举(可拓展)
PLAYER, //玩家
BARRIER, //障碍
NONE, //表示无类型
TELEPORT, //传送点
}EntityType;

typedef struct Chunk{ //区块结构体
EntityType type;
}Chunk;

有了以上的定义,因此我们引入结构体Instance(场景)

1
2
3
4
5
6
#define BLOCK_WIDTH_NUM 528
#define BLOCK_HEIGHT_NUM 288

typedef struct Instance{
Chunk chunk[BLOCK_HEIGHT_NUM][BLOCK_WIDTH_NUM];
}Instance;

实体&玩家

万物皆对象

万物皆实体。

很自然的,我们可以引入实体基类(可能有些许面向对象的想法)

1
2
3
4
5
6
typedef struct Entity{
//万物皆实体
double x,y; //实体在地图中的位置
PIMAGE texture; //实体纹理
EntityType type; //实体类型
}Entity;

其他实体可以通过包含此结构体来继承

从而得出玩家等实体的定义

1
2
3
4
5
6
7
typedef struct Player{
Entity base;
Movedir dir;
PIMAGE resourcePack[4][4];//关涉人物资源包
//玩家属性
//……
}Player;

交互式体验

有对象,才会有交互,自然我们应当引入NPC这一概念:

1
2
3
4
5
6
7
8
9
typedef struct Npc{
Entity base;
PIMAGE resourcePack[4][4];
//Npc属性
int hp;
//对话系统
char dialogText[20];
int dialogVisable = 0;
}Npc;

可以注意到,npc结构体中我加入了对话的变量,通过改变变量,我们可以达成交互的目的。

RPG(2d)实现的初步难点

  • 玩家视角&镜头视角

rpg中最重要的,最毋庸置疑的,应当是我们能够看到角色的交互和动作,这点理所当然,所以我们要引入镜头这一概念。

在这里我们会面临一大选择:镜头跟随玩家?还是镜头独立于玩家?

后者无疑是更好的选择。首先镜头的独立使玩家有更好,更大的不被束缚的空间体验;其次,镜头独立于玩家,为后期的需求变化提供了可能(我们只需要将镜头视xy=玩家视角xy即可)。

1
2
3
4
5
6
//镜头移速
#define CAMERA_MOVE_SPEED 10
//玩家移速
#define PLAYER_MOVE_SPEED 10
double camera_x;
double camera_y;
  • 地图滚动

初步考虑了玩家和镜头关系后,接下来关心的自然是我们该如何用镜头xy去查看地图的各个角落

答案很简单:根据游戏窗口大小以及镜头xy动态加载场景的某一块就可。

1
2
3
4
5
6
#define MAP_WIDTH 13200
#define MAP_HEIGHT 7200
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600

putimage(0,0,WINDOW_WIDTH,WINDOW_HEIGHT,mainMap,camera_x,camera_y);//移动镜头位置
  • 画面渲染

一个良好的游戏制作流程应当包含以下三个流程

/

渲染应当独立于输入与数据处理,这样使得后期维护以及重构成为可能。

现在,试想一下ps中的图层相关的概念,游戏的渲染工作其实和图层的概念极其相似。

其中玩家的渲染还包括:玩家阴影渲染,玩家的悬挂物(装备等)渲染……

/

具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
********************************************
*主要渲染模块:
*渲染:玩家,地图,场景贴图,npc
*其中渲染的顺序尤其重要
*层叠(图层)式渲染
*参数 instance 渲染目标场景
*******************************************/
void render(Instance *instance){
cleardevice();
putimage(0,0,WINDOW_WIDTH,WINDOW_HEIGHT,mainMap,camera_x,camera_y);//移动镜头位置
if(is_debug) debug(instance);
renderPlayer();
}

ps:debug模式仅为测试使用可忽略

  • 简单的碰撞检测

现在回到起点,我们安排好了区块,但是区块可以做什么用呢?碰撞检测似乎是一个合理的选择。

我们只需实时获取玩家的区块位置并且通过玩家移动方向比对前方障碍,即可判断是否发生碰撞检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
typedef enum Movedir {
//移动方向枚举
DOWN, LEFT, RIGHT, UP
} Movedir;

/**
**************************************
*碰撞检测模块:
*主要检测玩家和区块barrier之间的碰撞
*返回1,0
*参数
*instance 玩家此时所在场景
*dir 玩家此时的移动方向
*************************************/
int isHit(Instance *instance, Movedir dir) {//判断是否碰撞障碍
//获取玩家的区块坐标位置
int blockX = getEntityBlockX(player1.base.x);
int blockY = getEntityBlockY(player1.base.y);
switch (dir) {
case UP:
if (instance->block[blockY - 1][blockX].type == BARRIER)
return 1;
break;
case DOWN:
if (instance->block[blockY + 1][blockX].type == BARRIER)
return 1;
break;
case RIGHT:
if (instance->block[blockY][blockX + 1].type == BARRIER)
return 1;
break;
case LEFT:
if (instance->block[blockY][blockX - 1].type == BARRIER)
return 1;
break;
}
return 0;
}

ps:block只因当时忘了chunk。

接下来我们可以看看效果

现在是不存在障碍的。

点击方块,标记目标区块为barrier。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
**************************************
*debug模式主函数
*负责debug的一系列操作
*************************************/
void debug(Instance *instance) {
mouse_msg msg = {0};
int flag = 0;
int x, y;
while (mousemsg()) {//获取鼠标消息
msg = getmouse();
if (msg.is_down() && msg.is_left()) { //判断鼠标是否按下
flag = 1;
}
}
if (flag) {
//获取鼠标位置,计算区块位置并改变区块性质
x = (msg.x + camera_x) / BLOCK_SIZE;
y = (msg.y + camera_y) / BLOCK_SIZE;
if (x < BLOCK_WIDTH_NUM && y < BLOCK_HEIGHT_NUM) {
if (instance->block[y][x].type == BARRIER)
instance->block[y][x].type = NONE;
else
instance->block[y][x].type = BARRIER;
}
sprintf(format, "X = %d Y = %d", x, y);//格式化镜头等位置信息
}
renderBlock(*instance);
}

玩家移动在碰到红色方块后停止。

我们可以借此对平面地图添加barrier达到场景立体化效果。

ps:不要践踏草地