前言
身边一些C++初学者看完理论后觉得没有什么项目经验,学习到的知识没地儿练手不说,在找工作时也很受阻,于是便拿出来这个自己刚学习时写的小项目,很适合初学者练手。
整个项目是一个网络游戏框架,能把学到的TCP/IP通信、MySQL数据库以及c++类的概念和多线程理念应用进去,以此复习同时也总结一下所有的知识,因此诞生了此系列文章。
游戏主题构架主要参考http://blog.csdn.net/u010505527/article/details/17227695,本着巩固以及提升自己写代码和调试的能力,因此本代码是在阅读该篇文章的基础上全部重新手敲,主体架构相同但细节几乎完全不同,并加入了自己的一些想法如按住方向键暂时加速、回车暂停、每得10分提升难度以及分数记录等等部分,当然,还有后面进行网游化的通信代码部分。
一、MAIN函数的主体构成
贪吃蛇这个游戏基本上大部分人都玩过,具体就不多提了,游戏架构设计部分完全基于C++面向对象的思想,将问题逐步分解,首先确定main函数的运行过程,再根据需求以此设计需要用到的类,当然,最初的MAIN函数并不完全,只确定了主体逻辑,之后进行了完善:
#include "Game.h"
void main()
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if (WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
CONSOLE_CURSOR_INFO mycursor;
GetConsoleCursorInfo(hout, &mycursor);
mycursor.bVisible = false;
SetConsoleCursorInfo(hout, &mycursor);
Game mygame;
mygame.newgame();
mygame.gconnect();
mygame.login();
mygame.loaddata();
system("cls");
while (true) {
mygame.paintwindow();
mygame.set();
mygame.playgame();
if (!mygame.playagain())
break;
system("cls");
mygame.reset();
};
}
前面几行代码是sockets在windows下编程的初始化过程,之后将光标设置为不可见,新建一个game类,先完成绘制logo、连接服务器、登陆、载入服务器数据等一次化工作,之后进入一个循环,绘制游戏边界、设置、进入game运行的核心函数playgame,若游戏死亡则进入playagain函数询问是否再来一局,是则清屏并重置游戏,否则结束游戏。
###二、GAME类的设计
根据main函数的需求来设计GAME类,可以确定基本需要的数据和接口以及相应的功能,依次完成即可。
#pragma once
#include "TCPhead.h"
#include "Food.h"
#include "Snake.h"
#include <fstream>
#include <string>
#define NDEBUG
using namespace std;
class Game
{
public:
Game();
~Game();
void newgame();
void gconnect();
void login();
void loaddata();
void paintwindow();
void set();
void playgame();
bool playagain();
void reset();
void accelerate()
{
speed -= speed / 3;
level++;
}
bool eaten()
{
return (snake.Head() == food.foodposition());
}
bool hit()
{
//撞墙
int x, y;
x = snake.Head().xx();
y = snake.Head().yy();
if (x == 2 || x == 100)
return true;
if (y == 3 || y == 30)
return true;
//撞自己
return snake.hititself();
}
//状态栏
void showstate()
{
SetOutputposition(2, 2, hout);
cout << "Difficulty: " << level;
SetOutputposition(48, 2, hout);
cout << "Scores: " << score;
SetOutputposition(88, 2, hout);
cout << "Record: " << record;
}
bool senddata();
private:
int speed;
short level;
Snake snake;
Food food;
unsigned short score;
unsigned short record;
int sockfd;
};
常用函数定义为内联函数,节省系统资源,gconnect、login、loaddata和senddata这几个函数为通信用,暂时不做解释,来看看游戏运行最主要的几个函数:
####1、newgame()、paintwindows()
void Game::newgame() {
//SNAKES LOGO
SetConsoleTextAttribute(hout, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
SetOutputposition(34, 6, hout);
cout << " ■■■ ■ ■ ■■■ ■ ■ ■■■■";
SetConsoleTextAttribute(hout, FOREGROUND_GREEN);
SetOutputposition(34, 7, hout);
cout << "■ ■ ■■ ■ ■ ■ ■ ■ ■";
SetOutputposition(34, 8, hout);
SetConsoleTextAttribute(hout, FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■";
SetOutputposition(34, 9, hout);
SetConsoleTextAttribute(hout, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
cout << " ■■■ ■ ■ ■ ■■■■ ■■ ■■■";
SetOutputposition(34, 10, hout);
SetConsoleTextAttribute(hout, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << " ■ ■ ■■ ■ ■ ■ ■ ■";
SetOutputposition(34, 11, hout);
SetConsoleTextAttribute(hout, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY);
cout << "■ ■ ■ ■ ■ ■ ■ ■ ■";
SetOutputposition(34, 12, hout);
SetConsoleTextAttribute(hout, FOREGROUND_BLUE | FOREGROUND_INTENSITY);
cout << " ■■■ ■ ■ ■ ■ ■ ■ ■■■■";
int count = 6;
int interval = 600;
while (true) {
//闪动字
SetOutputposition(45, 24, hout);
cout << "press any key to start...";
Sleep(interval);
for (int i = 0; i != 25; i++)
cout << "\b \b";
if (_kbhit()) {
if (count-- == 0)
break;
interval = 50;
}
Sleep(interval);
if (_kbhit()) {
if (count-- == 0)
break;
interval = 50;
}
}
while (_kbhit()) //清除输入流中的数据
_getch();
}
newgame负责游戏SNAKES logo的绘制和闪动字的输出,值得注意的是最后用循环清除输入流中的数据,避免缓存中的数据对之后的部分产生影响。paintwindows()不多做解释,用于之后边框的绘制,与newgame中绘制snakes类似,setoutputposition函数在后文中解释。
####2、set()、reset()
void Game::set() {
SetOutputposition(20, 20, hout);
cout << "please choose difficulty:";
SetOutputposition(50, 21, hout);
cout << "1:easy" << endl;
SetOutputposition(50, 22, hout);
cout << "2:normal" << endl;
SetOutputposition(50, 23, hout);
cout << "3:hard";
choose:
char k = _getch();
switch (k)
{
case '1':
level = 1;
speed = 300;
break;
case '2':
level = 2;
speed = 200;
break;
case '3':
level = 3;
speed = 134;
break;
default:
goto choose;
}
SetOutputposition(45, 20, hout);
for (int i = 0; i != 25; i++)
cout << "\b \b";
SetOutputposition(60, 21, hout);
for (int i = 0; i != 10; i++)
cout << "\b \b";
SetOutputposition(60, 22, hout);
for (int i = 0; i != 10; i++)
cout << "\b \b";
SetOutputposition(60, 23, hout);
for (int i = 0; i != 10; i++)
cout << "\b \b";
}
void Game::reset()
{
snake.reset();
food.reset();
score = 0;
}
set()主要用于初始难度选择,其实之后加入难度升级机制后这个部分可以略去,reset()用于每次重新游戏时的数据的重置。
####3、playgame()、playagain()
void Game::playgame() {
food.foodpaint(hout); //绘制食物
snake.paintbody(hout); //绘制蛇身
showstate();
int speedr(speed);
while (true) {
char k;
speed = speedr;
while (_kbhit()) {
k = _getch();
if (k == '\r')
system("pause>>nul");
if(snake.changedirection(k))
break;
if (snake.press(k)) {
speed = 50;
}
}
snake.update(); //更新蛇出现的位置
if (hit()) //判断死亡
break;
if (!eaten()) {//判定吃食物
snake.tailclear(hout);
snake.painthead(hout);
}
else
{
score++;
if ((score / 10 - level) > 0) {
accelerate();
speedr = speed;
}
showstate();
food.update();
while (snake.overlap(food.foodposition()))//食物与蛇身重合
food.update();
food.foodpaint(hout);
}
senddata();
Sleep(speed);
}
}
bool Game::playagain()
{
if (score > record) {
record = score;
SetOutputposition(45, 14, hout);
cout << "NEW RECORD!!!" << endl;
}
SetOutputposition(45, 17, hout);
cout << "GAME OVER!" << endl;
SetOutputposition(45, 18, hout);
cout << "play again ? Y or N" << endl;
while (1) {
char k = _getch();
if (k == 'Y' || k == 'y')
return true;
else if (k == 'N' || k == 'n')
return false;
}
}
playgame()这个函数是这个类的最主要组成部分,整体逻辑结构如下图,只要判定没有撞墙或者撞自己,则置于一个永恒的循环中,同时包含判定是否吃到食物,是否按下方向键,是否转换方向等的处理代码。playagain()则比较简单,不多做赘述。
####4、其他函数
主要用于判定吃食物、撞墙或撞自己以及状态栏显示,代码比较简单,不多说。
###三、Food类的设计
从Game类中对Food类和Snake类的需求来创建这两个类,首先是Food类:
#pragma once
#include "Point.h"
class Food
{
public:
Food(int a = 14, int b = 15) :food{ a,b } {}
void foodpaint(HANDLE hand)
{
food.paint(hand);
}
const Point& foodposition()
{
return food;
}
void update()
{
food.xx() = 4+rand() % 48*2;
food.yy() = 4 + rand() % 26;
}
bool sendfood(const int &sock);
bool recvdata(int &sock);
void reset()
{
food = { 14,15 };
}
private:
Point food;
};
这个类整体结构比较简单,主要是完成它的绘制、刷新和位置返回的接口,刷新部分用到随机数生成函数,这个部分一般需要定义一个起点,这里没有做,只是简单调用会导致每次游戏食物依次出现的位置次序是重复的。