经过六天的艰苦(抓狂)后,终于完成了一个简单的联机版贪吃蛇制作,我将和大家分享一下制作的流程与感触。
1.题目难点
贪吃蛇是一款很经典的游戏,也是一个难度系数尚可,适宜初学c++者进行挑战的一个项目。在此过程中,主要的难点有两个:
1.如何让蛇动起来
2.如何让两台机器可以随时保持数据的交换
2.基本思路
对于电脑来说,蛇是不可能一点点蠕动的。所以在我的思考中,蛇每移动一步,就等同于在蛇头前面增加一格,同时将尾部删除。食物的产生用srand()和rand()函数来完成(虽然在最后为了保证两台机器食物同步我暂时删除了srand())。至于控制蛇,我就用了电脑的wsad四键,结合_getch()函数完成控制。至于最后的联机部分我们稍后再说。
3.对象&头文件
我总共构建了四个对象
-
框架
-
食物
-
蛇节点
-
蛇
头文件很多,在此对几个特殊的进行介绍。
1.#include <cstdlib>&&#include<ctime> :产生随机数
2.#include<WinSock2.h>&&include <WS2tcpip.h> :联机所用
3.#include<conio.h> :打印和游戏处理所用
4.详解
(因为是初学者操作,为了操作方便,所有类内成员全部为public,类与类之间也全部为友远关系)
1.框架是在控制台打印的东西,也是游戏的界面,我用全局变量二维数组作为整体的框架(42*42),以下是他的基本组成
class Frame {
public:
friend class snakeNode;
friend class snake;
void makeframe();//画出游戏的边框
void printframe();//打印
};
2.食物是贪吃蛇中必不可少的东西,食物的组成也不算复杂,如下
class Food {
public:
friend class Frame;
friend class snake;
//static int i;
int fx, fy;//记录事物的坐标
void randomFood();//产生随机食物
bool havefood();//判断食物是否被吃掉
void docter();//防止两个食物随机产生在同一位置
};
3.节点相对于说是一个类,其更像是一个结构,他是蛇整体的基本组成部分,相对于蛇来说更加具体形象。
class snakeNode {
public:
int x, y;//坐标
snakeNode(int ix, int iy, snakeNode* n, snakeNode*p, char snakeMood = '*') :x(ix), y(iy), next(n), prec(p) {
window[ix][iy] = snakeMood;
}//将节点在frame中表现出来
snakeNode *next, *prec;//用链表的结构来保存蛇
};
4.蛇是最庞大的类,其内的元素是对蛇的所有操作
class snake {
public:
string name;
friend class Frame;
friend class Food;
char snakeMood;//蛇的样子
snakeNode* head = new snakeNode(20, 20, nullptr, nullptr, snakeMood);
//为了方便所有蛇都默认从此点出发
void move();//游戏的关键
void addHead();//为move服务的函数
void detail();//为move服务的函数
enum Direction dir;//蛇运动的方向
bool block();//判断是否相撞
bool outofFrame(int h, int w);//判断是否出界
void changeDir1(char key);//键盘操作转换
};
下面对几个最重要的函数进行详解
1.move()
void snake::move() {
this->addHead();//先增加一格子
if (outofFrame(head->x, head->y) || block()) {
system("cls");
cout << "Game Over!" << endl;
system("pause");
exit(0);
}
//因为是由单机版改来的所以这里还没有修改成判断哪一方获胜的函数。。。
if (!food1.havefood()/*吃了食物*/) {
food1.randomFood();
}
else if (!food2.havefood()/*吃了食物*/) {
food2.randomFood();
}
else/*没吃则将尾部删除*/ {
detail();
}
}
2.randomfood()
void Food::randomFood() {
// srand((unsigned)time(0) * 1000000);
bool onSnake = true;
while (onSnake) {
onSnake = false;
fx = rand() % 40 + 1;
fy = rand() % 40 + 1;
if (window[fx][fy] == 'm') {
onSnake = true; break;//若重复了再来
}
//若在蛇身上,重来
for (snakeNode *snake = qqq.head; snake; snake = snake->next) {
if (fx == snake->x && fy == snake->y) {
onSnake = true; break;
}
}
for (snakeNode *snake = sss.head; snake; snake = snake->next) {
if (fx == snake->x && fy == snake->y) {
onSnake = true; break;
}
}
}
window[fx][fy] = 'm';
}
//PS:qqq和sss为两条蛇名
5.Tcp联机实现
针对socket内容,我只是停留在理解其用法的地步,现在正在看winsock一书,希望看完后能对其产生更为深刻的理解。
基本服务器和客户端的构建
//服务器
/*定义相关变量*/
char temp = 'd';
int sock_client;
struct sockaddr_in server_addr;
int addr_len = sizeof(struct sockaddr_in);
/*初始化*/
WSADATA wsaDate;
WORD wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaDate) != 0) {
cout << "加载失败!\n";
return 0;
}
/*创建套接字*/
if ((sock_client = socket(AF_INET, SOCK_STREAM, 0))<0) {
cout << "创建套接字失败! \n";
WSACleanup();
return 0;
}
/*填写服务器地址*/
char IP[20];
cout << "请输入服务器地址:";
cin >> IP;
memset((void *)&server_addr, 0, addr_len);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, IP, &server_addr.sin_addr.s_addr);
/*与服务器建立连接*/
if (connect(sock_client, (struct sockaddr*)&server_addr, addr_len) != 0) {
cout << "连接失败,错误代码:" << WSAGetLastError() << endl;
closesocket(sock_client);
WSACleanup();
return 0;
}
//客户端
char temp1;
SOCKET sock_server, newsock;
struct sockaddr_in addr;
struct sockaddr_in client_addr;
char msg[] = "Connect succeed. \n";
/*初始化*/
WSADATA wsaDate;
WORD wVersionRequested = MAKEWORD(2, 2);
if (WSAStartup(wVersionRequested, &wsaDate) != 0) {
cout << "加载失败!\n";
return 0;
}
/*创建套接字*/
if ((sock_server = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) {
cout << "创建套接字失败! \n";
WSACleanup();
return 0;
}
/*添加本地地址*/
int addr_len = sizeof(struct sockaddr_in);
memset((void *)&addr, 0, addr_len);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock_server, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
cout << "地址绑定失败,错误代码:" << WSAGetLastError() << endl;
WSACleanup();
return 0;
}
//将套接字设置为监听状态
if (listen(sock_server, 0) != 0) {
cout << "listen函数调用失败,错误代码:" << WSAGetLastError() << endl;
closesocket(sock_server);
WSACleanup();
return 0;
}
while (true) {
if ((newsock = accept(sock_server,(struct sockaddr*)&client_addr, &addr_len)) == INVALID_SOCKET) {
cout << "accept函数调用失败,错误代码:" << WSAGetLastError() << endl;
}
else {
cout << "成功接收到一个连接请求;\n";
break;
}
}
//部分代码来自于winsock编程一书改编而来
相对于socket的基本构造,如何交换数据才是重中之重。windows下的recv()(接受信息函数)是阻塞的(就是在没有信息发来时会一直卡在那里不动)。所以为了保证游戏的连贯与一致,游戏中数据的交换只有蛇的方向。我们要保证信息一直在不停的发送与接收中。在编码过程中,最耗时的部分就是实现如何保证两边总能接收到信息而不发生阻塞,最后我采用了一种规则:先发送再接受,这样根据数学逻辑来讲,在缓存区中存放的数据中的send是大于等于recv的,这样就可以保证总有一端可以收到信息,而在其收到后又会再极短的时间内发出信息,这样自然能保证游戏的流畅。
system("mode con cols=100 lines=50");
Frame frame;
char key;
char sended[1000], geted[1000];
frame.makeframe();
food1.randomFood();
food2.randomFood();
cout << "请输入sss的方向:";
cin >> key;
sended[0] = key;