《游戏设计之路》-不来推推箱子么?

大家好,失踪人口回归。新的一年,新的坑位(不是),今年就让我来带领小伙伴们一起设计一款独立游戏吧(* *)。


言归正传,说起游戏,大家第一印象可能就是次时代主机,PC主机或手机上那些画面绚丽,剧情丰富的游戏:如《GTA系列》,《赛博朋克2077》,《梦幻西游》,《塞尔达传说》等等。但可惜的是,单单靠一个人,是不可能完成这样的大型游戏的。不是技术上的问题,而是人力与成本上的问题。

但不要灰心,市面上也有很多仅靠1,2个人做出的很多精美的甚至获得国际大奖的独立游戏:如《小小梦魇》,《地狱边境》等等。

但我们要记住:万丈高楼起于垒土。作为一个初学者,我们需要学习的知识还有很多。所以不要着急,让我们从地基开始,打好基础,一步一步地向最高峰发起进攻。


一些题外话:游戏设计光靠一片热血是不行的,一些必备的基础知识则是必需的。例如基本的C++,JAVA等计算机语言的掌握,基本的开发环境的安装等。这里我默认读者是掌握了这些知识的。


·创建项目

首先,我们需要在VS上创建一个空的控制台应用

接着我们可以给项目取一个名字,这里我就叫这个小游戏为《回家吧-小箱子》

项目名称为“GoHome_SmallBox”

在项目创建成功后,我们可以看到VS自动为我们创建了一个GoHome_SmallBox.cpp文件

#include 


int main()
{
    std::cout << "Hello World!
";
}

我们将上面的代码修改一下:

#include 
using namespace std;


int main()
{
    char c;
    cin >> c;
    cout << c;
    return 0;
}

cin,cout都是istream类和ostream类的全局变量,我们在包含头文件iostream后,并声明using namespace std,就可以在任何位置使用了。

cin通过>>将输入值写入变量c,cout通过<<将变量值c输出。

对于大部分游戏程序来说,整个项目可以分为三步操作:

while(true){
getInput(); // 获取键盘或者鼠标的输入信息
updateGame(); // 根据输入信息对游戏内容进行修改
draw(); // 对修改后的游戏内容进行绘制并输出结果
}

·游戏内容设计

我们设定箱子为符号Y,箱子的最终目的地为符号X,当箱子到达目的地后,X变为Z,表示箱子成功回到家。而推动的小人则设定为P。

思考:当我们设计好基本内容后,还需要进行进一步的思考

  1. 当P到达边界后,继续向边界移动会发生什么?
  2. 当P推动两个箱子时,会发生什么?
  3. 朝着边界推箱子,会发生什么?

假如不解决上面的一些问题,我们直接进行编码实现的话,就会发现箱子有时候会飞出边界,人物P也会移动到边界之外。这样一来,就不符合我们所设定的游戏规则了。


·程序设计

首先我们来定义一些基本的初始变量:

// #表示墙壁,p表示玩家,X表示目的地,Y表示箱子
const char gSceneData[] = "
########

# XX p #

#  YY  #

#      #

########";
const int gSceneWidth = 8;
const int gSceneHeight = 5;
enum Object {
  OBJ_SPACE, // 空白空间
  OBJ_WALL,  // 墙壁 #
  OBJ_GOAL,  // 目标点 X
  OBJ_BOX,   // 盒子 Y
  OBJ_BOX_ON_GOAL,  // 盒子在目标点处 Z
  OBJ_PLAYER,  // 玩家 p
  OBJ_PLAYER_ON_GOAL,  //  玩家在目标点处 P
  OBJ_UNKNOW  // 未知
};

这里我把最初的初始场景数据放在了一个全局变量char数组gSceneData中。然后我们定义了一个枚举变量,这里面使用了几个枚举值描述了所有的游戏状态。

接下就是我们的主循环的代码

int main()
{
  // 创建初始状态数组
  Object* state = new Object[gSceneWidth * gSceneHeight];
  // 初始化场景
  initialize(state, gSceneWidth, gSceneHeight, gSceneData);
  // 游戏主循环
  while (true) {
    // 绘制
    draw(state, gSceneWidth, gSceneHeight);
    // 通关检测
    if (check(state, gSceneWidth, gSceneHeight)) {
      break;
    }
    // 获取输入
    cout << "w:向上;a:向左;s:向下;d:向右,请输入:" << endl;
    char input;
    cin >> input;
    // 更新数据
    update(state, input, gSceneWidth, gSceneHeight);
  }
  // 胜利后的信息
  cout << "恭喜你成功过关!" << endl;
  delete[] state;
  state = 0;
  return 0;
}

注意到其中调用到了下面四个方法:

// 初始化方法
void initialize(Object* state, int w, int h, const char* sceneData) {
  const char* index = sceneData;
  int x = 0;
  int y = 0;
  while (*index != '') { // 当字符不为NULL时
    Object t;
    switch (*index) {
    case '#': {
      t = OBJ_WALL;
      break;
    }
    case ' ': {
      t = OBJ_SPACE;
      break;
    }
    case 'X': {
      t = OBJ_GOAL;
      break;
    }
    case 'Y': {
      t = OBJ_BOX;
      break;
    }
    case 'Z': {
      t = OBJ_BOX_ON_GOAL;
      break;
    }
    case 'p': {
      t = OBJ_PLAYER;
      break;
    }
    case 'P': {
      t = OBJ_PLAYER_ON_GOAL;
      break;
    }
    case '
': { // 到下一行
      x = 0; // x返回最左边
      ++y; // y进入下一行
      t = OBJ_UNKNOW; // t暂时无数据
      break;
    }
    default: {
      t = OBJ_UNKNOW; // t为非法数据
    }
    }
    ++index;
    if (t != OBJ_UNKNOW) { // 若t为非法数据,则略过
      state[y * w + x] = t;// 向状态数据写入数据,这里的位置表示向下第y行,向右第x列的位置。这里是将一个二维数据存放在了一个一维数组里面
      ++x;
    }
  }
}
// 绘制,将state数组的数据绘制在控制台中
void draw(const Object* state, int w, int h) {
  // 按照枚举值的顺序定义该数组
  const char front[] = { ' ','#','X','Y','Z','p','P' };
  for (int y = 0; y < h; ++y) {
    for (int x = 0; x < w; ++x) {
      Object o = state[y * w + x];
      cout << front[o];
    }
    cout << endl;
  }
}
// 通关检测
bool check(const Object* state, int w, int h) {
  // 检查若没有盒子后,则通关
  for (int i = 0; i < w * h; ++i) {
    if (state[i] == OBJ_BOX) {
      return false;
    }
  }
  return true;
}
// 更新数据
void update(Object* state, char input, int w, int h) {
  // 首先获取移动的变换量
  int dx = 0;
  int dy = 0;
  // 这里我们注意一下,我们假设的是左上角为坐标原点,
  // 那么向上移动,y则为-1;向下移动,y则为1,y轴正向为向下
  // 向左移动,x为-1;向右移动,x为1
  switch (input) {
  case 'a':dx = -1; break;// 向左移动
  case 'd':dx = 1; break;// 向右移动
  case 'w':dy = -1; break;// 向上移动
  case 's':dy = 1; break;// 向下移动
  }
  // 查询玩家坐标,这里其实可以设置一个全局变量,记录上一次玩家所在位置,这样就不用每次来寻找一次玩家的位置
  int i = -1;
  for (i = 0; i < w * h; ++i) {
    if (state[i] == OBJ_PLAYER || state[i] == OBJ_PLAYER_ON_GOAL) {
      break;
    }
  }
  int x = i % w;// 小人的x轴位置应当为i对宽度的余数
  int y = i / w;// 小人的y轴位置应当为i对宽度的商


  //玩家移动后的坐标
  int tx = x + dx;
  int ty = y + dy;
  // 对玩家的位置进行判断
  if (tx < 0 || ty < 0 || tx >= w || ty >= h) {
    return;
  }
  // 移动位置为空白或者是目标点,则玩家移动
  int p = y * w + x;// 玩家的位置
  int tp = ty * w + tx;// 玩家移动后的位置
  if (state[tp] == OBJ_SPACE || state[tp] == OBJ_GOAL) {
    state[tp] = (state[tp] == OBJ_GOAL) ? OBJ_PLAYER_ON_GOAL : OBJ_PLAYER;// 若移动位置为目标点,则变为玩家站在目标点;否则则为玩家本身
    state[p] = (state[p] == OBJ_PLAYER_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;// 若当前位置为目标点,则变为目标点;否则变为空白位置
  }
  else if (state[tp] == OBJ_BOX || state[tp] == OBJ_BOX_ON_GOAL) { // 如果移动位置为箱子,或者为箱子在目标点,并且箱子的下一个位置为空白或目标点,则可以移动
    int tx2 = tx + dx;
    int ty2 = ty + dy;
    // 检查移动位置同方向的下一个位置是否为合法位置
    if (tx2 < 0 || ty2 < 0 || tx2 >= w || ty2 >= h) { //按键无效
      return;
    }
    int tp2 = ty2 * w + tx2;// 移动位置同方向上的下一个位置
    if (state[tp2] == OBJ_SPACE || state[tp2] == OBJ_GOAL) {
      // 按顺序更改三个位置的数据
      state[tp2] = (state[tp2] == OBJ_GOAL) ? OBJ_BOX_ON_GOAL : OBJ_BOX;
      state[tp] = (state[tp] == OBJ_BOX_ON_GOAL) ? OBJ_PLAYER_ON_GOAL : OBJ_PLAYER;
      state[p] = (state[p] == OBJ_PLAYER_ON_GOAL) ? OBJ_GOAL : OBJ_SPACE;
    }
  }
}

好啦,今天的任务就算完成啦。但我们可以发现,这仅仅是一个初始版的游戏,还有很多可以优化改进的地方。那么我们下一次还将继续深入,对我们的第一个小游戏进行优化。

谢谢阅读(* *)

展开阅读全文

页面更新:2024-04-23

标签:箱子   同方   小游戏   游戏   标点   空白   位置   目标   玩家   项目

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top