用C++编写游戏容易吗?有什么开源的小游戏吗?能分享一下吗? 这个答案中,我提到学习游戏编程可从回合制游戏开始,例如井字棋。
考虑到一些初学者的学习需求,我就写一个井字棋的教程吧。
关于怎么快速学C/C++,可以加下小编的C/C++学习群:341+636+727,不管你是小白还是大牛,小编我都欢迎,不定期分享干货,欢迎初学和进阶中的小伙伴。
每天晚上20:00都会开直播给大家分享C/C++游戏编程学习知识和路线方法,群里会不定期更新最新的教程和学习方法,最后祝所有程序员都能够走上人生巅峰,让代码将梦想照进现实
1. 游戏状态的表示
首先,我认为表示方法(representation)是编程中应最先要考虑的事情。对于回合制游戏,我们需要存储一个回合中的游戏状态(game state)。
以下用一个结构体表示井字棋一个回合中的状态,并加入函数作初始化:
typedef struct {
int board[3][3]; // -1 = empty, 0 = O, 1 = X
int turn; // O first} state;void init(state* s) {
int i, j;
for (j = 0; j < 3; j++)
for (i = 0; i < 3; i++)
s->board[j][i] = -1;
s->turn = 0;}
以上用二维数组存储棋盘(board)是其中一种表示方式,另一种方式则是记录每个回合下棋子的位置。我们采用前者是因为它较容易实现胜负判定。有些回合制游戏可能使用冗余的表示方式,以方便实现各种规则。
而使用结构体而不是直接用全局变量,可带来一些优点,例如增强可读性及内聚性。
2. 显示游戏状态
编写游戏时,我们通常希望先显示游戏状态,之后才加入其他规则,因为这样可以方便测试。
我希望用这样的文本显示游戏状态,当空置时写上位置编号(1-9),以方便玩家输入下棋位置:
1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9
简单直白地编写代码的话:
void display(const state* s) {
int i, j;
for (j = 0; j < 3; j++) {
for (i = 0; i < 3; i++) {
switch (s->board[j][i]) {
case -1: printf(" %d ", j * 3 + i + 1); break;
case 0: printf(" O "); break;
case 1: printf(" X "); break;
}
if (i < 2)
printf("|");
else
printf("\n");
}
if (j < 2)
printf("---+---+---\n");
else
printf("\n");
}}
由于 display()
只读而不改变游戏状态,所以其参数类型为 const state*
。
我们稍压缩一下代码:
void display(const state* s) {
int i, j;
for (j = 0; j < 3; printf(++j < 3 ? "---+---+---\n" : "\n"))
for (i = 0; i < 3; putchar("||\n"[i++]))
printf(" %c ", s->board[j][i] == -1 ? '1' + j * 3 + i : "OX"[s->board[j][i]]);}
我们可以加入 main()
函数去显示初始化的状态:
int main() {
state s;
init(&s);
display(&s);}
3. 实现下棋
然后,我们加入第一个游戏规则,就是下棋:
int move(state* s, int i, int j) {
if (s->board[j][i] != -1)
return 0;
s->board[j][i] = s->turn++ % 2;
return 1;}
函数内做了一个合法性判断,如果该位置已有棋子,则返回 0 表示失败。成功的话,在偶数回合填入 0,表示 O;奇数回合填入 1,表示 X;然后都把回合加一。
更改 main()
简单测试:
int main() {
state s;
init(&s);
display(&s);
move(&s, 1, 1);
display(&s);
move(&s, 0, 1);
display(&s);}
输出:
4. 处理输入
在每一回合中,提示当前玩家(O 或 X),并让玩家输入一个下棋位置(1-9),如果位置不合法,则重新输入:
void human(state* s) {
char c;
do {
printf("%c: ", "OX"[s->turn % 2]);
c = getchar();
while (getchar() != '\n');
printf("\n");
} while (c < '1' || c > '9' || !move(s, (c - '1') % 3, (c - '1') / 3));}
在标准输入中,要到回车键才能处理输入,所以这里我们读了第一个输入字符后,就忽略其他字符直到读到换行符。我们把表示位置的字符转换成二维数组索引。
然后,就可以修改 main()
实现二人下棋的流程:
int main() {
state s;
init(&s);
display(&s);
while (s.turn < 9) {
human(&s);
display(&s);
} }
5. 胜负判定
众所周知,井字棋的胜利条件,是有三个棋子在横线、直线或斜线连成一线。我们实现一个 evaluate()
函数去评估棋局的状态,如果 O 胜出则返回 1,X 胜出则返回 -1,不分胜负则返回 0:
#define CHECK(j1, i1, j2, i2, j3, i3) \ if (s->board[j1][i1] != -1 && s->board[j1][i1] == s->board[j2][i2] && s->board[j1][i1] == s->board[j3][i3]) \ return s->board[j1][i1] == 0 ? 1 : -1;int evaluate(const state* s) {
int i;
for (i = 0; i < 3; i++) {
CHECK(i, 0, i, 1, i, 2); // horizontal
CHECK(0, i, 1, i, 2, i); // vertical
}
CHECK(0, 0, 1, 1, 2, 2); // diagnoal
CHECK(0, 2, 1, 1, 2, 0); // diagnoal
return 0;}
上面的代码使用了一个宏 CHECK()
去检测三个位置是否都为相同的棋子,如是则直接返回胜方。
最后,我们在 main()
中,待每次下棋及显示状态后, 判定是否出现胜方,如果到达第 9 个回合(回合从 0 开始),则判定是平局(draw):
int main() {
state s;
init(&s);
display(&s);
while (s.turn < 9) {
human(&s);
display(&s);
switch (evaluate(&s)) {
case 1: printf("O win\n"); return 0;
case -1: printf("X win\n"); return 0;
}
}
printf("Draw\n");}
6. 总结
本篇实现了二人井字棋,它是一个简单的回合制游戏。我们先选择了游戏的状态表示方式(state
结构体及init()
函数),然后把状态以文本形式显示(display()
函数),加入每回合下棋规则(move()
函数),以及人类玩家的输入处理(human()
函数),并作胜负判定(evaluate()
函数),最后在main()
里则实现了按回合的循环及输出胜负结果。
虽然这个游戏本身以及 60 行的示例代码都很简单,但这个框架可以用于实现其他(更复杂的)回合制游戏。实时游戏(如动作游戏)的主要区别,其实也只在于把输入部分做成非阻塞的函数,而该循环则称为游戏循环(game loop)。