游戏设计之路不来推推箱子么?
大家好,失踪人口回归。新的一年,新的坑位(不是),今年就让我来带领小伙伴们一起设计一款独立游戏吧(*╹▽╹*)。
言归正传,说起游戏,大家第一印象可能就是次时代主机,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。
思考:当我们设计好基本内容后,还需要进行进一步的思考 当P到达边界后,继续向边界移动会发生什么? 当P推动两个箱子时,会发生什么? 朝着边界推箱子,会发生什么?
假如不解决上面的一些问题,我们直接进行编码实现的话,就会发现箱子有时候会飞出边界,人物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; } } }
好啦,今天的任务就算完成啦。但我们可以发现,这仅仅是一个初始版的游戏,还有很多可以优化改进的地方。那么我们下一次还将继续深入,对我们的第一个小游戏进行优化。
谢谢阅读(*╹▽╹*)