1.原因:
因疫情原因,很多公司的年会取消了,修改为线上。就像线上有什么好玩的,突然想到外国主播,让水友发弹幕来控制游戏人物行动这件事情。感觉还挺好玩的。
就想是怎么做出来的。可以做一个这种类型的小游戏,如果公司年会线上的话,可以让不同团队来控制这个小人物,看看那个团队的统一性,凝聚力最强。(原理是一段时间内,得到命令最多的那个命令,便是角色下一步的行动方向),可以扩展为web类型,或者QT,MFC做出UI都可以。
2.原理:
Game类,绘制游戏界面,逻辑运动处理等。
Client类,发送接收界面的命令,并链接Game联动出画面效果
Common文件,设置公共的消息类型,数据,方法等
Service类,服务器类,响应客户端发出的命令,并作出筛选,发出给所有客户端同步绘制
3.实现:
Game01.h
#pragma once
#include<cstdio>
#include<string>
#include"Common.h"
#define MAP_VALUE 50
using namespace std;
class Game01
{
public:
Game01();
~Game01();
void init(); //初始化地图和坐标数据
void createMap(); //随机生成地图
void refreshMap(NodeData *data); //刷新地图,重绘当前位置
bool MapIsOk(); //判断生成地图是否可用,不可用则完善地图通道,使其可通过
bool IsSuccess(); // 是否到达终点
void GetPositionData(int* command); //获取服务器发送数据,并转换为下一步坐标
void SetPosittionData(); // 给服务器发送下一步命令
void PrintfMap(); //输出地图
void setNewX(int x)
{
m_newX = x;
}
void setNewY(int y)
{
m_newY = y;
}
int getNewX()
{
return m_newX;
}
int getNewY()
{
return m_newY;
}
private:
int m_newX; //当前X坐标
int m_newY; //当前Y坐标
int m_beginX; //起始X,Y
int m_beginY;
int m_endX; //终点X,Y
int m_endY;
int m_MAP[MAP_VALUE][MAP_VALUE];
};
Game02.cpp
#include "Game01.h"
#include <ctime>
#include"Client.h"
Game01::Game01()
{
//初始化地图和坐标数据
m_newX = 0;
m_newY = 0;
m_beginX = 0;
m_beginY = 0;
m_endX = 0;
m_endY = 0;
memset(m_MAP, 0, sizeof(m_MAP));
}
Game01::~Game01()
{
}
void Game01::init()
{
createMap(); //生成地图
//if (MapIsOk()) //完善地图通道
//{
// int command[2] = { 0 };
// while (!IsSuccess()) //未到达终点
// {
// GetPositionData(command);
// if (command[0] == m_newX && command[1] == m_newY)
// {
// continue; //与当前坐标一致,则不移动
// }
// else
// {
// m_newX = command[0]; //接受服务器数据重新定位的当前位置
// m_newY = command[1];
// //refreshMap();
// }
// }
//}
}
void Game01::createMap() //随机生成地图
{
int64_t new_time = 0;
int randVlaue = 0;
int commont[2] = { 0 };
m_newY = 1;
m_beginY = 1;
m_endX = 48;
m_endY = 49;
m_MAP[0][1] = 0; //起始位置
m_MAP[48][49] = 0; //终点位置
while (!IsSuccess()) //未到达终点)
{
GetPositionData(commont);
if (commont[0] == m_newX && commont[1] == m_newY)
{
continue; //与当前坐标一致,则不移动
}
else if (commont[0] == m_endX && commont[1] == m_endY)
{
m_newX = commont[0]; //接受服务器数据重新定位的当前位置
m_newY = commont[1];
m_MAP[m_newX][m_newY] = 2;
}
else if ((commont[0] <= 0 || commont[0] >= 49 || commont[1] <= 0 || commont[1] >= 49))
{
continue;
}
//else if (m_MAP[commont[0]][commont[1]] == 2)
//{
// continue;
//}
else
{
//if(0)
m_newX = commont[0]; //接受服务器数据重新定位的当前位置
m_newY = commont[1];
m_MAP[m_newX][m_newY] = 2;
//refreshMap();
}
}
for (int i = 0; i < MAP_VALUE; i++)
{
for (int j = 0; j < MAP_VALUE; j++)
{
if (i == 0 || i == 49 || (i > 0 && j == 0) || (i > 0 && j == 49))
{
m_MAP[i][j] = 1;
}
else
{
//srand((int)time(0)); // 产生随机种子 把0换成NULL也行
new_time = random(10000000);//GetSysTimeMicros();
randVlaue = random(100);
if(m_MAP[i][j] != 2)
m_MAP[i][j] = ((new_time * randVlaue) % 37 > 17 ? 0 : 1); //随机生成迷宫数据 // 取余数 暂且可以预留为 难度标准
//○
}
}
}
//m_newY = random(49);
//m_beginY = m_newY;
//m_endX = random(49);
//m_endY = 49;
//m_MAP[0][m_beginY] = 0; //起始位置
//m_MAP[m_endX][49] = 0; //终点位置
m_newX = 0;
m_newY = 1;
m_beginY = 1;
m_endX = 48;
m_endY = 49;
m_MAP[0][1] = 0; //起始位置
m_MAP[48][49] = 0; //终点位置
m_MAP[m_newX][m_newY] = 3;
PrintfMap();
}
void Game01::PrintfMap() //输出地图 0 可移动路径 1 墙壁 2 可通行路径 3 当前位置
{
system("cls");
for (int i = 0; i < MAP_VALUE; i++)
{
for (int j = 0; j < MAP_VALUE; j++)
{
if (m_MAP[i][j] == 1)
{
printf("■");
}
else if (m_MAP[i][j] == 2)
{
//printf("○");
printf(" ");
}
else if (m_MAP[i][j] == 3)
{
printf("○");
}
else
{
printf(" ");
}
}
printf("\n");
}
}
void Game01::refreshMap(NodeData *data) //刷新地图,重绘当前位置
{
char massage = data->massgeData[0];
//给后出现的客户端统一数据
//m_newX = data->New_X;
//m_newY = data->New_Y;
//int command
if (data->massage == MassgeType::COMMAND && data->massgeData == "OK")//是否成功
{
printf("成功了\n");
}
if (data->massage == MassgeType::DIRECTION)
{
switch (massage)
{
case 'W':
case 'w':
if (m_newX > 0 && m_MAP[m_newX - 1][m_newY] != 1) {
m_newX = m_newX - 1;
m_MAP[m_newX + 1][m_newY] = 2;
}
break;
case 'S':
case 's':
if (m_newX < 49 && m_MAP[m_newX + 1][m_newY] != 1) {
m_newX = m_newX + 1;
m_MAP[m_newX - 1][m_newY] = 2;
}
break;
case 'A':
case 'a':
if (m_newY > 0 && m_MAP[m_newX][m_newY - 1] != 1) {
m_newY = m_newY - 1;
m_MAP[m_newX][m_newY + 1] = 2;
}
break;
case 'D':
case 'd':
if (m_newY < 49 && m_MAP[m_newX][m_newY + 1] != 1) {
m_newY = m_newY + 1;
m_MAP[m_newX][m_newY - 1] = 2;
}
break;
}
m_MAP[m_newX][m_newY] = 3;
PrintfMap();
IsSuccess();
}
}
bool Game01::MapIsOk() //判断生成地图是否可用,不可用则完善地图通道,使其可通过
{
bool flag = false;
int randVlaue = random(4);
while (m_newX != m_endY && m_newY != m_endY) //未到达终点
{
randVlaue = random(4);
switch (randVlaue % 4)
{
case 0:
break;
case 1:
break;
case 2:
break;
case 3:
break;
}
}
return flag;
}
bool Game01::IsSuccess() // 是否到达终点
{
bool flag = false;
if (m_newX == m_endX && m_newY == m_endY) // 到达终点,给服务器发送OK数据
{
SetPosittionData();
flag = true;
}
return flag;
}
void Game01::GetPositionData(int* command) //获取服务器发送数据,并转换为下一步坐标
{
//int command[2] = {0}; //0为X,1为Y
//调用客户端类的单例 获取到massage w a s d
char massge=' ';
// srand((int)time(0)); // 产生随机种子 把0换成NULL也行
int randVlaue = random(100000)%10;
if (randVlaue == 0 || randVlaue == 1)
massge = 'w';
else if (randVlaue == 2 || randVlaue == 3 || randVlaue == 4)
massge = 's';
else if (randVlaue == 6 || randVlaue == 5)
massge = 'a';
else// (randVlaue == 3)
massge = 'd';
//if (randVlaue == 0 )
// massge = 'w';
//else if (randVlaue == 1 /*|| randVlaue == 4*/)
// massge = 's';
//else if (randVlaue ==2)
// massge = 'a';
//else// (randVlaue == 3)
// massge = 'd';
switch (massge)
{
case 'W':
case 'w':
command[0] = m_newX;
command[1] = m_newY - 1;
break;
case 'S':
case 's':
command[0] = m_newX;
command[1] = m_newY + 1;
break;
case 'A':
case 'a':
command[0] = m_newX -1;
command[1] = m_newY;
break;
case 'D':
case 'd':
command[0] = m_newX + 1;
command[1] = m_newY;
break;
}
}
void Game01::SetPosittionData() // 给服务器发送下一步命令
{
//调用客户端类的单例
NodeData data;
data.massage = MassgeType::COMMAND;
data.massgeData = "SUCCESS";
Client::GetInfrance()->SendData(&data);
}
Client.h
#pragma once
// 从 Windows 头文件中排除极少使用的信息
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<string>
#ifdef _WIN32
#include<WinSock2.h>
#include<Windows.h>
#else
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#define SOCKET int
#define SOCKET_ERROR -1
#endif
#pragma comment(lib,"ws2_32.lib")
#include"Common.h"
using namespace std;
class Client
{
public:
void Init();
void Connect();
void Close();
void SendData(NodeData *data);
NodeData* RecvData();
static Client* GetInfrance();
static Client* m_client;
private:
Client() {}
SOCKET sock;
sockaddr_in sockadd;
bool isClose = false;
NodeData nodeData;
};
Client.cpp
#include "Client.h"
Client* Client::m_client = nullptr;
void Client::Init()
{
if (!isClose)
{
//启动windows socket
#ifdef _WIN32
WORD var = MAKEWORD(2, 2);
WSADATA dar;
WSAStartup(var, &dar);
#endif
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockadd.sin_family = AF_INET;
sockadd.sin_port = htons(6666);
#ifdef _WIN32
sockadd.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //转换IP地址为网络字节数据
#else
sockadd.sin_addr.s_addr = inet_addr("192.168.126.128"); //192.168.128.1
#endif
}
}
Client* Client::GetInfrance()
{
if (m_client == nullptr)
{
m_client = new Client();
}
return m_client;
}
void Client::Connect()
{
if (!isClose)
{
//连接服务器
if (SOCKET_ERROR == connect(sock, (const sockaddr*)&sockadd, sizeof(sockaddr_in)))
{
printf("错误,连接服务器失败!\n");
}
else
{
printf("连接服务器成功!\n");
}
}
}
void Client::Close()
{
//关闭套接字
//getchar();
#ifdef _WIN32
closesocket(sock);
WSACleanup();
#else
close(sock);
#endif
isClose = true;
}
void Client::SendData(NodeData *data)
{
if (!isClose)
{
if (data != nullptr)
{
send(sock, (char*)data, sizeof(NodeData), 0);
}
}
}
NodeData* Client::RecvData()
{
if (!isClose)
{
memset(&nodeData,0,sizeof(NodeData));
//接收发送数据
if (recv(sock, (char*)&nodeData, sizeof(NodeData), 0) > 0)
{
printf("服务端发送的数据,IP:%s\n", nodeData.massgeData.c_str());
return &nodeData;
}
}
return nullptr;
}
Common.h
#pragma once
#include<string>
#define random(x) rand()%(x)
enum MassgeType
{
COMMAND=0, //命令
DIRECTION, //方向
};
typedef struct NodeData
{
MassgeType massage = COMMAND; //消息类型
std::string massgeData; //消息数据
int New_X;
int New_Y;
};
Service.cpp //懒得封装了,直接干在一个文件里面
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#else
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#define SOCKET int
#define SOCKET_ERROR -1
#define INVALID_SOCKET (SOCKET)(~0)
#endif
#include<vector>
#include<map>
#include<string>
#define random(x) rand()%(x)
using namespace std;
enum MassgeType
{
COMMAND = 0, //命令
DIRECTION, //方向
};
typedef struct NodeData
{
MassgeType massage = COMMAND; //消息类型
std::string massgeData; //消息数据
int New_X;
int New_Y;
};
std::vector<SOCKET> g_socket; //总的连接客户端
//std::vector<SOCKET> g_socket_Client; //发送数据的客户端
std::map<std::string, int> MessageList;
int MessageData(SOCKET socket)
{
static long time_stamp = GetCurrentTime();//getCurrentTime为上面介绍的函数;
static int count1, count2, count3, count4 = 0;
static int new_X, new_Y = 0; //第一个客户端当前的XY坐标
//==========================================================
//要执行的代码
//缓冲区
char szRecv[1024] = { 0 };
int nLen = recv(socket, szRecv, sizeof(NodeData), 0);
NodeData *data = (NodeData*)szRecv;
if (nLen <= 0)
{
printf("客户端已退出\n");
return -1;
}
if (socket == g_socket[0])
{
new_X = data->New_X;
new_Y = data->New_Y;
}
//if (g_socket_Client.end() == find(g_socket_Client.begin(), g_socket_Client.end(), socket)) //当前集合中不存在时,加入
//{
// g_socket_Client.push_back(socket);
//}
if (data->massage == DIRECTION)
{
//消息发送接受处理
std::string sss = data->massgeData;
switch (sss[0])
{
case 'W':
case 'w':
MessageList.insert(pair<std::string, int>(sss, count1++));
break;
case 'S':
case 's':
MessageList.insert(pair<std::string, int>(sss, count2++));
break;
case 'A':
case 'a':
MessageList.insert(pair<std::string, int>(sss, count3++));
break;
case 'D':
case 'd':
MessageList.insert(pair<std::string, int>(sss, count4++));
break;
}
//printf("客户端发送的数据,IP:%d , Data:%s, X:%d, Y:%d\n", socket, data->massgeData.c_str(), data->New_X,data->New_Y);
}
else if (data->massage == COMMAND && data->massgeData=="SUCCESS")
{
NodeData recvData;
recvData.massage = COMMAND;
recvData.massgeData = "OK";
recvData.New_X = new_X;
recvData.New_Y = new_Y;
for (int i = 0; i < g_socket.size(); i++)
{
send(g_socket[i], (char*)&recvData, sizeof(NodeData), 0);
}
printf("服务端发送的数据,IP:%d , Data:%s\n", socket, recvData.massgeData.c_str());
return 1;
}
//==========================================================
//计算睡眠时间(毫秒),保证每隔500ms循环一次,发送一次客户端需求最多的命令 是客户端实行
int dis = GetCurrentTime() - time_stamp;
if (dis >= 500 && MessageList.size() > 0) {
//求四个方向最多的数 即为下一步方向
NodeData recvData;
recvData.massage = DIRECTION;
recvData.New_X = new_X;
recvData.New_Y = new_Y;
int num = max(max(max(count1, count2), count3), count4);
if (num == count1)
{
recvData.massgeData = "W";
}
else if (num == count2)
{
recvData.massgeData = "S";
}
else if (num == count3)
{
recvData.massgeData = "A";
}
else if (num == count4)
{
recvData.massgeData = "D";
}
// if (g_socket.size() >= 10) //压测代码
// {
for (int i = 0; i < g_socket.size(); i++)
{
send(g_socket[i], (char*)&recvData, sizeof(NodeData), 0);
printf("服务端发出的数据,IP:%d , Data:%s\n", g_socket[i], recvData.massgeData.c_str());
}
//}
count1 = 0;
count2 = 0;
count3 = 0;
count4 = 0;
MessageList.clear();
time_stamp = GetCurrentTime();//getCurrentTime为上面介绍的函数;
}
return 1;
}
int main()
{
#ifdef _WIN32
//启动windows socket
WORD var = MAKEWORD(2, 2);
WSADATA dar;
WSAStartup(var, &dar);
#endif
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sockadd;
sockadd.sin_family = AF_INET;
sockadd.sin_port = htons(6666);
SOCKET maxsock = sock;
#ifdef _WIN32
sockadd.sin_addr.S_un.S_addr = INADDR_ANY; //inet_addr("127.0.0.1"); 转换IP地址为网络字节数据
#else
sockadd.sin_addr.s_addr = INADDR_ANY;
#endif
//绑定socket
if (SOCKET_ERROR == bind(sock, (sockaddr*)&sockadd, sizeof(sockaddr_in)))
{
printf("错误,绑定端口失败!\n");
}
else
{
printf("绑定端口成功!\n");
}
//开启监听
if (SOCKET_ERROR == listen(sock, 10))
{
printf("错误,监听端口失败!\n");
}
else
{
printf("监听端口成功!\n");
}
while (true)
{
//开启select 等待消息
fd_set dataRead;
fd_set dataWrite;
fd_set dataError;
timeval t_val = { 1,0 };
//初始化
FD_ZERO(&dataRead);
FD_ZERO(&dataWrite);
FD_ZERO(&dataError);
//绑定到sock
FD_SET(sock, &dataRead);
FD_SET(sock, &dataWrite);
FD_SET(sock, &dataError);
//将socket集合中的socket添加到可读集合中
for (int i = 0; i < (int)g_socket.size(); i++)
{
FD_SET(g_socket[i], &dataRead);
if (maxsock < g_socket[i])
maxsock = g_socket[i];
}
int ret = select(maxsock + 1, &dataRead, &dataWrite, &dataError, &t_val);
if (ret < 0)
{
//有错误
printf("错误,select出错...\n");
// int nErr = WSAGetLastError();
// printf("select: %d, %s\n", nErr, strerror(errno));
//break;
}
//判断sock是否可读,有则,游客户端接入
if (FD_ISSET(sock, &dataRead))
{
FD_CLR(sock, &dataRead);
//accept 等待接收客户端连接
sockaddr_in sockadd_Client = {};
int clientLen = sizeof(sockaddr_in);
//接收客户端信息的socket
SOCKET _cSocket = INVALID_SOCKET;
#ifdef _WIN32
_cSocket = accept(sock, (sockaddr*)&sockadd_Client, &clientLen);
#else
_cSocket = accept(sock, (sockaddr*)&sockadd_Client, (socklen_t*)&clientLen);
#endif
if (INVALID_SOCKET == _cSocket)
{
printf("错误,接收到无效客户端socket...\n");
}
printf("新客户段接入,socketID:%d,IP:%s \n", (int)_cSocket, inet_ntoa(sockadd_Client.sin_addr));
char buff[128] = { 0 };
strcpy(buff, inet_ntoa(sockadd_Client.sin_addr));
for (int i = 0; i < (int)g_socket.size(); i++)
{
send(g_socket[i], buff, 128, 0);
}
g_socket.push_back(_cSocket);
}
//消息处理
for (int i = 0; i < (int)dataRead.fd_count; i++)
{
if (-1 == MessageData(dataRead.fd_array[i])) //客户端无消息
{
auto iter = find(g_socket.begin(), g_socket.end(), dataRead.fd_array[i]);
if (iter != g_socket.end()) //但是sockrt在连接列表里面 则移除
{
g_socket.erase(iter);
}
}
}
// printf("未处理网络请求\n");
}
getchar();
#ifdef _WIN32
//关闭套接字
closesocket(sock);
//接收发送数据
WSACleanup();
#else
close(sock);
#endif
return 0;
}
4.运行
5:问题
还存在很多问题,不过大体是实现了,测试10多个客户端也OK,上百个,电脑性能不行,怕GG,不过应该没啥问题。
1.多个客户端一起运行时,先运行的人物位置和后运行的人物位置不同步。
2.地图生成虽然是随机的,但是随机函数存在一定问题,所以地图经常是很相似,甚至不变的。
3.未实现客户端退出命令等,还是输入命令必须是asdw ,其他不识别。可以通过做UI时 button的点击等修改,也懒得完善了。
4.当然还有一些其他的问题,目前还没有发现,毕竟我不是测试。。。。。。。后面可能会有GAme02,Game03等。