一些关于c++游戏的杂谈
1. 前言
:::warning[声明]
- 转载文章请经过 z_z_b_ 同意谢谢,毕竟是耗费了很久写的。不过附赠的代码、项目、文件之类的随便用。
- 编译环境均为 c++17(14应该也行),编译器均为 dev。至于为什么不是 vsc,因为当时不喜欢。特殊编译环境会额外提及。
- 所有内容都是 z_z_b_ 两年前自学的。可能有错误的地方,欢迎指出。有不理解的也可以评论在下面。z_z_b_ 看到了会回答。但是请不要私信谢谢,懒得删。
- 文章是在家里写的,只有cp没法运行,所以代码都是用几年前写的老版本整合在一起的,有些估计会有问题,同样欢迎指出。 :::
其实要说真正写好一篇,z_z_b_ 似乎也没做到像胎神一样留下一堆可玩性很强的遗产,毕竟也是当年的一份愿望。
虽然感觉这些现在的用处也没多大了,好像还不如 AI。但是好歹也是一份遗产,对于某些需要的人来说还算是有点用的。某C姓网站莫名其妙把之前写的改成 vip 关键我还不知道怎么改回来……那就全部重构成一整份吧(其实还有一个点,当年写的我现在完全读不下去,好**二的语言)。
整篇文章基本是休闲娱乐,对各位大佬的竞赛没有丝毫帮助。如果你看完想尝试,也尽量在家里不要在课上。虽然我感觉可能回家就直接启动了喵。
所以代码 Dev 均可运行,谁说 dev 不如 vsc 了。
整篇只涉及少量对于游戏写法的指点,大部分是对你所写的游戏界面的优化。不过 c++ 写的游戏能写成胎神那样差不多了,也别强求用这玩意写出什么高级到一种境界的。
其实所有游戏都是大模拟(废话)。如果你 OI 一般的话也很难写出一份比较不错的游戏,所以你可以先去做一道猪国杀体验一下大致的难度。而且说实话到后面的内容也并不简单,可能你套模板都不会套……
整篇文章 z_z_b_ 也还是花了很多玩终末地1999明日方舟原神崩铁的时间,从接近二十万字的内容里面整理出来的,所以不喜勿喷,如果你觉得我写的不好那就是你说的对,谢谢喵。
呵呵,没想过能留下来的遗产是这些东西。
所有文件都在 这里 自取。(密码:d7dr)
其中动物园怪谈那个文件夹密码是 5ou6。里面内容需全部下载并放在同一个文件夹中才能运行。
由于蓝奏云限制,只能上传 txt,自己转转谢谢喵。
2. 常用优化
1. 基础优化
这里主要包含了非常非常基础的一些界面上的调整。大部分需要
#include<windows.h>
以及需要注意的事情是 bits/stdc++.h 不包含 windows.h,所以请加在后面谢谢喵。
除了第一个以外,后面的都和 system 有关。可以稍微科普一下,其实 system 的头文件是 cstdlib 而不是 windows.h 。不过我记得是包括在 bits 里面的,就不用额外加了。
所以我们先来看停顿,这个很基础,过一下就行。
Sleep(n);//停顿 n ms。注意S大写以及停顿为毫秒。n 为正整数。
一般常用的也就这个,在大部分场景下都适用喵。不过 z_z_b_ 建议在 while(1) 里面下意识加一个,不然可能会体验到极速版。
后面 system 就先一块说了。也没什么好分析的。
中括号括起来的内容都是可以替换的。可以自己运行一下试试。
system("title [标题]");//换标题
system("mode con cols=[数字] lines=[数字]");//设置运行框大小。数字代表这一列/行能装多少字符,可以通过这个来居中输出的文本。
system("cls");//清屏。注意之后会讲解更好的无闪清屏,不过并不是说 cls 不行,两者对应的区间不一样。
system("start [文件.后缀名]");//不加路径的话只有在同一个文件夹中才能起效。注意了。
system("TASKKILL /F /IM [文件.后缀名]");//其他的写法自己打开 cmd 发个 help 去看。
ok,那就可以进入正片了。需要注意的是,所有标题后面的括号都是函数名,也就是通常比较常见的命名。不过并不绝对,包括里面的变量名你不喜欢都可以改掉。
2. 无闪清屏(gotoxy)
首先你需要理解光标这个概念。你现在运行一个 exe,一闪一闪的就是光标。它可以表示你输入、输出的位置。
cls 在清屏上的劣势就在于:即使加了 Sleep 用以减缓速率,依然会出现闪屏的现象。
所以对此,我们可以改变光标位置,把输入输出的内容覆盖掉。
即为:
void gotoxy(int x,int y)//覆盖清屏 ,从指定行列覆盖。注意后文有总代码
{
COORD pos={(short)x,(short)y};
HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOut,pos);
return;
}
举个栗子喵:
while(1)
{
printf("1.给这篇文章点个赞吧\n2.关注z\_z\_b\_可以喵\n");
if(kd('1')||kd(VK_NUMPAD1)) cout<<"THANK YOU\n";//关于 kd 见第二条
if(kd('2')||kd(VK_NUMPAD2)) cout<<"THANK YOU SO MUCH\n";
cout<<" ";
gotoxy(0,0);
Sleep(1);
}
同样的,如果你是在写类似迷宫之类需要移动的,可以见后文的实例。
(这玩意感觉不举栗子还是很难讲的喵)
对于这个还有其他的用法,比如在某一指定位置输出什么内容,直接更改 gotoxy 里面的内容就行了。
比如把什么东西置中。不过我记得 windows.h 里面有一部分是专门和这类有关的。
3. 移除快速编辑&插入模式(noedit)
快速编辑模式,这个通俗一点就是你打开个cmd,鼠标一拖就能拖住一些格子,这个就是。
其实个人感觉没啥用处。如果你写一个游戏,尤其是涉及到需要点击的(比如后文的按钮),那么只要你点击运行框,整个界面都会优先让你先编辑你的,导致暂停住。
这个其实是可以手动关掉的,不过你也不可能让你的玩家去关掉对吧。
void noedit()//windows.h,同样后文有。
{
HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin,&mode);
mode&=~ENABLE_QUICK_EDIT_MODE;
mode&=~ENABLE_INSERT_MODE;
mode&=~ENABLE_MOUSE_INPUT;
SetConsoleMode(hStdin,mode);
}
这个就是个公式,直接在主函数里调用就行了。
需要注意的是这个不是像你手动关掉一样一劳永逸的。只会对单次生成的运行框起效。
4. 隐藏/显示光标(hide/show)
同样的,和 2.3 同理。
光标前文已经解释过了。
这个其实有好有坏吧,好处在于让你输入的时候知道你输到哪里了。坏处就是面对大范围的持续输出,光标会到处乱闪,影响体验。
void HideCursor()//windows.h
{
CONSOLE_CURSOR_INFO cur={1,0};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);
}
void ShowCursor()//windows.h
{
CONSOLE_CURSOR_INFO cur={1,1};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);
}
(其实 z_z_b_ 是喜欢写成 hide/show 的,不过容易重名喵。反正一篇只用一两次,将就一下了。)
同样的,只对单次生成的运行框有效,所以也就不需要在程序结尾的时候加一个 show 了。
5. 判断按键按下(kd)
其实 kd 是 keydown 的缩写喵。
关于判断输入,我们有很多种方式。
比如 cin、scanf,亦或是 getchar 之类的。
但是如果你需要写一个通过玩家键入按键来进行操作的,那上面三种我全部不建议。
给大家举一个栗子:
#include<bits/stdc++.h>
#include<windows.h>
using namespace std;
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)
int main()
{
while(1)
{
//1.
char c;
c=getchar();
if(c=='1') cout<<"1\n";
if(c=='2') cout<<"2\n";
//2.
if(kd(VK_NUMPAD1)||kd('1')) cout<<1<<"\n";
if(kd(VK_NUMPAD2)||kd('2')) cout<<2<<"\n";
//下面这个 Sleep 别注释
Sleep(50);
}
return 0;
}
大家可以运行一下,每次注释掉 1 或者 2 段落 中的一个,在长按 1 的同时时不时按一下 2(不用区分大小键盘)。
优势很明显:由于 getchar 每次循环只能输入一次,在 Sleep 后相当于一秒钟最多只能判断 20 次。
而 kd 不一样,kd 取决于你在单次循环中的个数,比如在这份代码中相当于判断 20 次是否键入 1 和 20 次是否键入 2 。
而在多人游戏里,getchar 会导致明显的识别不到按键,但是 kd 就不会。
其标准版上面的栗子里也展示了。
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)
直接用 define 就行,没必要写个函数。说明一下,其中的 VK_NONAME 是虚拟按键的常量。
:::info[常量&按键]
常量名 对应按键
————————————————————————
VK_LBUTTON 鼠标左键
VK_RBUTTON 鼠标右键
VK_CANCEL Ctrl+Break
VK_MBUTTON 鼠标中键
VK_BACK Backspace键
VK_TAB Tab 键
VK_RETURN 回车键
VK_SHIFT Shift键
VK_CONTROL Ctrl键
VK_MENU Alt键
VK_PAUSE Pause键
VK_CAPITAL Caps Lock键
VK_ESCAPE Esc键
VK_SPACE 空格键
VK_PRIOR Page Up键
VK_NEXT Page Down键
VK_END End键
VK_HOME Home键
VK_LEFT 左箭头键
VK_UP 上箭头键
VK_RIGHT 右箭头键
VK_DOWN 下箭头键
VK_SNAPSHOT Print Screen键
VK_Insert Insert键
VK_Delete Delete键
'0' – '9' 数字0-9
'A' – 'Z' 字母A - Z
VK_LWIN 左WinKey(104键盘才有)
VK_RWIN 右WinKey(104键盘才有)
VK_APPS AppsKey(104键盘才有)
VK_NUMPAD0 小键盘0键
VK_NUMPAD1 小键盘1键
VK_NUMPAD2 小键盘2键
VK_NUMPAD3 小键盘3键
VK_NUMPAD4 小键盘4键
VK_NUMPAD5 小键盘5键
VK_NUMPAD6 小键盘6键
VK_NUMPAD7 小键盘7键
VK_NUMPAD8 小键盘8键
VK_NUMPAD9 小键盘9键
VK_F1 - VK_F24 功能键F1–F24
VK_NUMLOCK Num Lock键
VK_SCROLL Scroll Lock键
:::
用法就是直接替换就行。注意一下,数字 0-9 和字母 A-Z 对用值中不要忘了打单引号。
以及 A-Z 其实包括了小写,只要键入这个键就行。但数字 0-9 不包括小键盘上面的,需要额外判断(建议一起写,不要单写)。
如果没懂,后面项目那里有实例。还没动发评论区问。
除此之外,还有一些并不常见的用法,这里仅提一嘴。有兴趣可以自己去看看。
#define down(VK_NONAME) keybd_event(VK_NONAME,0,0,0,0)//按下某个键,注意是按下不是判断按下
#define up(VK_NONAME) keybd_event(VK_NONAME,0,0,0,0)//松开某个键,注意是松开不是判断松开
#define press(VK_NONAME) up(VK_NONAME),down(VK_NONAME)//按下松开,为一次press。
注意,这三条均是模拟按键操作,不是判断。
以及:down 了之后请 up,要不你就直接用 press。
6. 颜色(color)
color 在 system 中其实似乎有的。
system("color [][]");//注意两个中括号是连一起的。
//举个例子:system("color 1A");
其中一个是字体颜色,一个是背景颜色。但是我忘了那个是了喵……
但是这个颜色是有限的。具体来说只有以下几种:
0=黑色 8=灰色 1=蓝色 9=淡蓝色 2=绿色 A=淡绿色 3=浅绿色 B=淡浅绿色 4=红色 C=淡红色 5=紫色 D=淡紫色 6=黄色 E=淡黄色 7=白色 F=亮白色
这个有个缺陷:就是每次修改颜色时会直接针对当前页面上所有进行修改,而不能仅对一部分进行修改。
或者是 z_z_b_ 不会。反正没下面的好用就对了喵。
介绍一个相对好用一些的:
void color(int a)
{
/*亮白*/ if(a==0) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE);
/*蓝色*/ if(a==1) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_GREEN|FOREGROUND_BLUE);
/*绿色*/ if(a==2) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_GREEN);
/*紫色*/ if(a==3) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_RED|FOREGROUND_BLUE);
/*红色*/ if(a==4) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_RED);
/*黄色*/ if(a==5) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_RED|FOREGROUND_GREEN);
/*深蓝色*/ if(a==6) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY|FOREGROUND_BLUE);
/*土黄色or金黄色*/ if(a==7) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_RED|FOREGROUND_GREEN);
/*灰色接近白*/ if(a==8) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE);
}
具体用法嘛,在你要修改颜色的前面直接 color([0~8]) 即可。可以单独对输出内容的颜色进行修改而不会影响前面已经输出的内容。
不过需要提醒一点:在改完颜色后加个 color(0) 是个好习惯喵。
其实理论上还有一种更复杂、涉及颜色更多的。其实也只有十六种颜色(但是可以修改背景颜色)
void color(int ForgC, int BackC)//SetColorAndBackground函数(颜色函数)
{
WORD wColor=((BackC & 0x0F)<<4)+(ForgC & 0x0F);
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), wColor);
return ;
//或者
HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(handle,ForeColor+BackGroundColor*0x10);
}
这个颜色对应的是 0-15 而不是 0~F(其实本质一样的喵,只不过这个传参传的是 int 而已)。
0=黑色 8=灰色 1=蓝色 9=淡蓝色 2=绿色 10=淡绿色 3=浅绿色 11=淡浅绿色 4=红色 12=淡红色 5=紫色 13=淡紫色 6=黄色 14=淡黄色 7=白色 15=亮白色
不过这个太不美观了(?,所以我习惯用 color。
7. 弹窗(MessageBox)
有点直白了,弹窗就是提示信息框(MessageBox)。
这其实是个 windows.h 自带的,所以介绍个用法就行了。
MessageBox(NULL,"内容","标题",类型|样式);
第一个 NULL 好像也对应着什么东西来着,我记得当时 CSDN 上有人给 z_z_b_ 提了一嘴的,不过忘了。
基本上写 NULL 其实已经覆盖了绝大多数的使用范围了。
内容和标题都好理解,这边提一下类型和样式。
类型即是对应按键类型。
MB_OK:只有一个按键,即确定
MB_ABORTRETRYIGNORE:有三个按键,分别是中止、重试和忽略
MB_OKCANCEL:有两个按键,确定和取消
MB_RETRYCANCEL:有两个按键,重试和取消
MB_YESNO:有两个按键,是和否
MB_YESNOCANCEL:有三个按键,是、否和取消
而样式就是正经弹窗的什么警告啊提示啊之类的,我也不知道怎么形容,大家可以自己试试 awa。
MB_ICONEXCLAMATION //警告图标 MB_ICONHAND //大红叉图标 MB_ICONQUESTION //问号图标 MB_ICONASTERISK //倒!图标
注意,类型|样式中间是个或运算喵。
其中还有一点:这个函数是由返回值的,用来辨别哪个按键被按下。
DABORT:中止按钮被选中;
IDCANCEL:取消按钮被选中;
IDIGNORE:忽略按钮被选中。
IDNO:否按钮被选中;
IDOK:确定按钮被选中;
IDRETRY:重试按钮被选中。
IDYES:是按钮被选中
所以用法即是:
if(MessageBox(NULL,"你好,这是一个提示信息框","信息框",MB_OK)==IDOK) MessageBox(NULL,"请按确定继续","next",MB_OK);
不过和正经函数一样,忽略返回值直接用也没有问题的喵。
8. 中文转 int
这个不知道该怎么形容了,偶然间发现的,实际上作用也不算大。
这个是 z_z_b_ 偶然发现的。一个中文可以被两个 int 类型的负数表示。
举个栗子喵:(z_z_b_ 并不知道这两个是啥,当时没写,cp 好像就不能这么用,早忘了也懒得下个 dev 运行。)
#include<cstdio>
int main(){printf("%c%c%c%c",-319,-67,-72,-10);}
要说这个用法估计就是什么特别需要隐藏的东西时候用了,比如什么问答之类的。
不如只下发 exe 不下发 cpp 管用。
倒是之前发现了一个新的用法,当时去下小说看,发现每章被压缩成一行了……
于是愤怒的写下了下面的代码:
#include<bits/stdc++.h>
#include<windows.h>
#define int long long
using namespace std;
namespace init
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000)?1:0)
#define sl(n) Sleep(n)
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init;
string s,t,r;
bool judge(char x)
{
return (int)x>=0;
}
bool check(int x,int y)
{
return ((x==-95&&y==-93)||(x==-93&&y==-95)||(x==-93&&y==-65)||(x==-95&&y==-79)||(x==-95&&y==-73));
}
signed main()
{
s="name";
t=s+"-new.txt";
s+=".txt";
freopen(s.c_str(),"r",stdin);
freopen(t.c_str(),"w",stdout);
while(getline(cin,r))
{
if(r.size()<=1000) puts(r.c_str());
else
{
for(int i=0;i<(int)r.size();i+=2)
{
if(judge(r[i]))
{
while(judge(r[i]))
{
cout<<r[i];
i++;
}
i-=2;
continue;
}
if(check(r[i],r[i+1]))
{
while(check(r[i],r[i+1]))
{
cout<<r[i]<<r[i+1];
i+=2;
}
i-=2;
puts("\n");
continue;
}
cout<<r[i]<<r[i+1];
}
puts("");
}
}
return 0;
}
但是我总感觉有更简单的写法喵。
算了,将就吧。
这个资源 z_z_b_ 之前统计了五千多个字,在文章开头可以自取。
需要注意的是输出的时候两个需要一起输出才能组成一个汉字喵。
9. 存读档
没啥好讲的,下面提供了两种写法。
真的不行你用 freopen 一个道理。注意 fclose 一下就行。
cd 和 dd 用的是 fscanf,fcd 和 fdd 用的是 finfout,看个人喜好了。
namespace stream
{
void cd(void)
{
FILE*fp=fopen("名字.txt","wb");
fprintf(fp,"%d",变量名);
fclose(fp);
}
void dd(void)
{
FILE*fp=fopen("名字.txt","rb");
if(fp!=NULL)
{
fscanf(fp,"%d",&变量名);
fclose(fp);
}
else{cd();printf("未找到存档,已新建");return;}
}
void fcd()
{
ofstream fout;
fout.open("路径+文件名",ios::out);
fout<<"喵?"<<endl;
fout.close();
}
void fdd()
{
ifstream fin;
fin.open("路径+文件名",ios::in);
if(!fin.is_open()) puts("cannot open the file");
if(!fin) puts("cannot open the file");
else
{
string k;
fin>>k;
cout<<k<<endl;
}
fin.close();
}
}
using namespace stream;
3. 算法优化
从现在开始每一层的内容就会更加困难了。
在继续看下去之前,需要你对 2.1~2.6 足够熟悉。不要求能默打出来,反正又不是比赛,但是需要你看见这些函数大概知道其的含义喵。
说是算法优化其实也不准确,有些确实算是,但是有些不算。不过共同的特点就是它们非!常!长!
而网页加载是有限的,内容越多越卡。所以部分代码我可能会省略掉一起放在资源里面。
如果你真的想每段都有代码来解释,可以看 CSDN。不过这几篇大概率都被神秘的上了 vip 关键还不会去掉。说一下不是本人上的,神秘 C 站自己上的。
1. 随机迷宫
前面先说一下,后面的代码里有些地方混用了,所以你可以看着改改。当时把三种写法融合在一起的时候没管那么多。
学这个的前提还有一个:你需要会 rand,就是随机。
篇幅有限不打算讲,可以自己搜搜。当然 mt19937 也可以。
不然你猜为什么叫随机迷宫?
当然 z_z_b_ 可能是为数不多会写这玩意的,在我写技巧6之前。
这个的本质其实很简单,不过写法比较多样,这里介绍三种(其实只有三种)。
我们先把整个地图用墙填满,然后每隔一个挖空(包括列),随后会形成类似于这样的地图:
#######
# # # #
#######
# # # #
#######
其实到这一步大家应该也能猜到了:从某个空格出发,每次打通墙,直到整个地图形成联通快。
由此延伸出 dfs、bfs 和并查集三种写法。不过需要注意的是,这三种写法不会有回路,并且要求迷宫大小一定是奇数
注:其实这个也可以不挖空,毕竟没啥区别,只是这样方便理解喵。
1. bfs
这边直接给出思路。毕竟如果有一定的竞赛基础还是很好想的:
1.让迷宫全是墙.
2.选起点作为迷宫的通路,然后把它的邻墙放入列表
3.当列表里还有墙时
1.从列表里随机选一个墙,如果这面墙分隔的两个单元格只有一个单元格被访问过
(a)那就从列表里移除这面墙,让未访问的单元格成为迷宫的通路
(b)把这个格子的墙加入列表
2.如果墙两面的单元格都已经被访问过,那就从列表里移除这面墙
关键代码其实就这一段:
while(where.size())
{
int r=rand()%(where.size());
qiang what=where[r];
x_p=what.x;
y_p=what.y;
switch(what.f)
{
case down:{x_p++;break;}
case right:{y_p++;break;}
case left:{y_p--;break;}
case up:{x_p--;break;}
}
if(dt[x_p][y_p]==WALL) dt[what.x][what.y]=dt[x_p][y_p]=KONG,put_wall();
where.erase(where.begin()+r);
}
应该比较好理解。总代码放在资源里一起了。不然文章会炸掉。
2. dfs
思路也是差不多的。
1.初始化地图,只有0和1的状态。其中,0和1分别代表道路和墙体,注意四周皆墙。(即前面所说的)
2.靠近边缘随机选取状态为1的道路点,作为起点 a
3.在起点 a 的上下左右四个方向,跨两个寻找同样为1的道路点 c
1. 如果找到,则将它们之间的墙体 b 打通,然后将 c 作为新的起点,然后继续进行第2步
2. 如果四个方向都没有找到,则不断回退到上一点,直到找到有一点其他方向满足条件,然后继续查询
4. 当查无可查的时候,迷宫也就填充完毕了喵
关键代码仍只有 dfs 这一段:
void dfs(int x,int y)
{
bool f[5]={0};
while(1)
{
if(f[0]&&f[1]&&f[2]&&f[3]) return ;
int r=rand()%4;
int nx=fx[r][0],ny=fx[r][1],zx=zj[r][0],zy=zj[r][1];
if(x+nx<1||x+nx>n||y+ny<1||y+ny>m) {f[r]=1;continue;}//return ;
if(dt[x+zx][y+zy]!=WALL||v[x+zx][y+zy]||v[x+nx][y+ny]) {f[r]=1;continue;}//return ;
f[r]=1;
dt[x+zx][y+zy]=KONG;
v[x+zx][y+zy]=v[x+nx][y+ny]=1;
dfs(x+nx,y+ny);
}
return ;
}
3. 并查集
这个有点意思,着重讲一下。我个人建议用这个,虽然这个对地图大小有一定限制,具体优劣会在后文提及。
1. 创建所有墙的列表(除了四边),并且创建所有单元的集合,每个集合中只包含一个单元。
2. 随机从墙的列表中选取一个,取该墙两边分隔的两个单元
1. 两个单元属于不同的集合,则将去除当前的墙,把分隔的两个单元连同当前墙三个点作为一个单元集合;并将当前选中的墙移出列表
2. 如果属于同一个集合,则直接将当前选中的墙移出列表
3. 不断重复第 2 步,直到所有墙都检测过
1.创建列表
初始化并查集,需要注意的是其实只查之周围两个单元就行了,没必要找四个,毕竟有重叠。
void init()
{
for(int i=1;i<=n*n;i++) fa[i]=i;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int d=0;d<2;d++)
if(i+dx[d]>0&&j+dy[d]>0&&i+dx[d]<=n&&j+dy[d]<=n)v.push_back((qiang){i,j,i+dx[d],j+dy[d]});
}
2.创建迷宫
这个相对来说比较容易,运用并查集的基本操作就行了。
while(!v.empty())
{
int r=rand()%v.size();
if(!judge(v[r])) merge(v[r]),mp[v[r].fx][v[r].fy][v[r].sx][v[r].sy]=KONG;
v.erase(v.begin()+r);
}
3.记录迷宫
虽然 mp 已经求出来了,不过这个很难观察,它记录的是两个单元之间的关系。
所以我们要把这个关系还原,即:
void vec_to_int()
{
int idx=1,jdx=0;
for(int i=1;i<=2*n+1;i++) dt[1][i]=WALL;
for(int i=1;i<=n;i++)
{
dt[i+idx][1]=WALL;
jdx=1;
for(int j=1;j<=n+1;j++) dt[i+idx][j+jdx]=KONG,((j!=n)&&(mp[i][j][i][j+1]==KONG))?dt[i+idx][j+(++jdx)]=KONG:dt[i+idx][j+(++jdx)]=WALL;
idx++,jdx=1;
for(int j=1;j<=n+1;j++) dt[i+idx][j+jdx]=WALL,((i!=n)&&(mp[i][j][i+1][j]==KONG))?dt[i+idx][j+(++jdx)]=KONG:dt[i+idx][j+(++jdx)]=WALL;
dt[2*n+1][2*n+1]=WALL;
}
dt[X][Y]=Player;
}
总代码仍然省掉,放资源里。
然后是对于总代码的一些说明:
1. 地图保存在数组 dt[][] 里,直接调用即可。不过分清上下界谢谢喵。
2. 并查集生成的地图大小最多到120左右(实际是124),不要往死里开。其他的量级都在500往上。
最后是一些对比:
| BFS生成 | DFS生成 | 并查集生成 | |
|---|---|---|---|
| 主路(一般) | 不明显 | 明显 | 不明显 |
| 岔路(一般) | 多 | 少 | 多 |
| 地图大小设置 | 0~1009 | 0~567 | 0~124 |
注:主路即为出路。由于 dfs 是一条路走到死的特性,所以 dfs 主路在量级较小时非常明显。以及对比仅为相对对比,不是绝对,毕竟这是个随机的。
2. 地牢
1. 原理
想法来源:房间和迷宫:一个地牢生成算法
之后根据里面所有的内容复刻了一个 c++ 版本的。
主体来说,分为以下几个部分:
- 生成房间:给定一个空白地图,在地图里进行n次尝试,每次生成一个处于随机位置的,随机大小的房间。如果它所在的位置没有被覆盖,那么它就是可行的。
- 生成迷宫:在剩下的地方按照 3.1 的内容生成迷宫。
- 连接房间与迷宫:对于每一个房间,遍历周围,找到能与之相连的路。然后玩运气,设定一个值,判断连接,保证至少有一条路与之连接。
- 删除死路:找到死路的末尾,不断删除直到不存在。
需要注意的是,第三点其实是有问题的,正确的写法应该为:把所有道路的末端均连接。注意是末端。
理解之后就可以写代码了。这个和 3.4 3.5 不一样,没什么好讲的,所以 z_z_b_ 这边呈现两份(不在资源里,自己复制一下),一份是观赏性的,一份是生成迷宫的。
2. 观赏
做了一些还不错的效果,个人感觉还挺好看的。
//建议在用这个生成地图的时候加一个判断是否全联通的搜索,zzb实验后发现有概率房间与道路不连通(zzb已经+了)
#define debug 4//停顿时间,最好0~10
#include<conio.h>
#include<windows.h>
#include<bits/stdc++.h>
using namespace std;
namespace init_ //小技巧15
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME)&0x8000)?1:0)
#define sl(n) Sleep(n)
#define cls system("cls")
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init_;
namespace Color//加上颜色更加美观
{
void color(int ForgC)
{
int BackC=0;
WORD wColor=((BackC&0x0F)<<4)+(ForgC&0x0F);
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),wColor);
}
}
using namespace Color;
#define m 239
#define n 61
#define WALL -1
#define KONG 2
bool D=0;
int t;
struct room_{int x,y,lx,ly;}rm[150];
struct play_er{int x,y;}start;
int dt[1010][1010];
int fx[5][5]={{2,0},{-2,0},{0,2},{0,-2}};
int zj[5][5]={{1,0},{-1,0},{0,1},{0,-1}};
bool v[1010][1010]={0};
play_er e[114514];
void print()
{
color(15);
gotoxy(0,0);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) printf("%c",dt[i][j]==KONG?'#':' ');
puts("");
}
}
void full()//全屏窗口,方便调试
{
system("mode con cols=50 lines=60");//随便设定一个值隐藏滚动条
HWND hwnd=GetForegroundWindow();
int cx=GetSystemMetrics(SM_CXSCREEN),cy=GetSystemMetrics(SM_CYSCREEN);//获得屏幕高度与宽度
LONG lw=GetWindowLong(hwnd,GWL_STYLE);
SetWindowLong(hwnd,GWL_STYLE,(lw|WS_POPUP|WS_MAXIMIZE)&~WS_CAPTION&~WS_THICKFRAME&~WS_BORDER);
SetWindowPos(hwnd,HWND_TOP,0,0,cx,cy,0);
}
CONSOLE_SCREEN_BUFFER_INFO getxy()//小技巧9
{
CONSOLE_SCREEN_BUFFER_INFO bInfo;
HANDLE hConsole=GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleScreenBufferInfo(hConsole,&bInfo);
return bInfo;
}
#define cx getxy().dwMaximumWindowSize.X
#define cy getxy().dwMaximumWindowSize.Y
void try_()//插房间,为了方便,房间的x,y坐标为偶数,长宽为奇数
{
memset(rm,0,sizeof rm);t=0;
for(int i=1;i<=1000;i++)
{
int x=rand()%cx,y=rand()%(cy-3),lx=rand()%11+20,ly=rand()%6+7;
bool f=1;
if(x%2!=0) x++;
if(y%2!=0) y++;
if(lx%2==0) lx++;
if(ly%2==0) ly++;
if(x+lx>=cx||y+ly>=cy-3) f=0;
for(int j=1;j<=t&&f;j++)
{
room_ r=rm[j];
if((max(x,r.x)<=min(r.x+r.lx,x+lx))&&(max(-r.y-r.ly,-y-ly)<=min(-r.y,-y))) f=0;
}
if(f) rm[++t]=(room_){x,y,lx,ly};
}
}
void dfs(int x,int y)//小技巧6
{
bool f[5]={0};
gotoxy(y-1,x-1),printf("#");
Sleep(debug/2);
while(1)
{
if(f[0]&&f[1]&&f[2]&&f[3]) return ;
int r=rand()%4;
int nx=fx[r][0],ny=fx[r][1],zx=zj[r][0],zy=zj[r][1];
if(x+nx<1||x+nx>n||y+ny<1||y+ny>m) {f[r]=1;continue;}//return ;
if(dt[x+zx][y+zy]!=WALL||v[x+zx][y+zy]||v[x+nx][y+ny]) {f[r]=1;continue;}//return ;
f[r]=1;
dt[x+zx][y+zy]=KONG;
v[x+zx][y+zy]=v[x+nx][y+ny]=1;
gotoxy(y+zy-1,x+zx-1),printf("#");Sleep(debug/2);//输出
gotoxy(y+ny-1,x+nx-1),printf("#");Sleep(debug/2);//输出
dfs(x+nx,y+ny);
}
return ;
}
void init()
{
memset(v,0,sizeof v);//一般的初始化
start.x=2,start.y=2;
memset(dt,WALL,sizeof dt);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i==1||i==n||j==1||j==m) dt[i][j]=WALL;
if(i%2==0&&j%2==0) dt[i][j]=KONG;
else dt[i][j]=WALL;
}
}
for(int i=1;i<=t;i++)//输出并标记房间
{
color(rand()%14+1);
for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++) dt[y][x]=KONG,v[y][x]=1,gotoxy(x-1,y-1),printf("#");
Sleep(debug*10);
}
gotoxy(0,0);
for(int i=0;i<=n;i++)//建路
for(int j=1;j<=m;j++)
if(dt[i][j]==KONG&&dt[i-1][j]==WALL&&dt[i+1][j]==WALL&&dt[i][j-1]==WALL&&dt[i][j+1]==WALL) color(rand()%14+1),dfs(i,j);
}
void make_way()//造路
{
init();
gotoxy(0,0);
color(0);
// for(int i=1;i<=n;i++)
// {
// for(int j=1;j<=m;j++) printf("%c",dt[i][j]==KONG?'#':' ');
// puts("");
// }
}
void connect()//连接路与房间
{
color(4);
const int ch=3;//3%的概率选中
int sum=0;
for(int i=1;i<=t;i++)//枚举房间
{
//分别枚举上面的边,左边的边,下面的边和右边的边
//先找能与之相连的总数
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++)
{
if(dt[y][rm[i].x-2]==KONG) sum++;
if(dt[y][rm[i].x+rm[i].lx+1]==KONG) sum++;
}
for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)
{
if(dt[rm[i].y-2][x]==KONG) sum++;
if(dt[rm[i].y+rm[i].ly+1][x]==KONG) sum++;
}
//暴力枚举连边
bool b=0;
while(!b)
{
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++)
{
if(dt[y][rm[i].x-2]==KONG)
{
if(rand()%100<ch||sum==1) dt[y][rm[i].x-1]=KONG,gotoxy(rm[i].x-1-1,y-1),printf("#"),b=1;
if(rand()%100<ch||sum==1) {sum--;continue;}
sum--;
}
if(dt[y][rm[i].x+rm[i].lx+1]==KONG)
{
if(rand()%100<ch||sum==1) dt[y][rm[i].x+rm[i].lx]=KONG,gotoxy(rm[i].x+rm[i].lx-1,y-1),printf("#"),b=1;
if(rand()%100<ch||sum==1) {sum--;continue;}
sum--;
}
}
for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)
{
if(dt[rm[i].y-2][x]==KONG)
{
if(rand()%100<ch||sum==1) dt[rm[i].y-1][x]=KONG,gotoxy(x-1,rm[i].y-1-1),printf("#"),b=1;
if(rand()%100<ch||sum==1) {sum--;continue;}
sum--;
}
if(dt[rm[i].y+rm[i].ly+1][x]==KONG)
{
if(rand()%100<ch||sum==1) dt[rm[i].y+rm[i].ly][x]=KONG,gotoxy(x-1,rm[i].y+rm[i].ly-1),printf("#"),b=1;
if(rand()%100<ch||sum==1) {sum--;continue;}
sum--;
}
}
}
}
gotoxy(0,0);
color(15);
return;
}
void get_end()//取出死路位置,但注意之前的代码有bug,现在改了
{
int sx=0,sy=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(dt[i][j]==KONG) sx=i,sy=j;
queue<play_er> q;
while(!q.empty()) q.pop();
bool vis[1010][1010];
memset(vis,0,sizeof vis);
q.push((play_er){sx,sy});
vis[sx][sy]=1;
while(!q.empty())
{
play_er p=q.front();
q.pop();
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(!vis[X][Y]&&dt[X][Y]==KONG)
{
q.push((play_er){X,Y});
vis[X][Y]=1;
}
}
//现在删路是判断周围是否只有一个通路
int f=4;
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(dt[X][Y]==KONG) f--;
}
if(f==3) e[++t]=p;
}
}
void try__()//反复删除
{
get_end();
while(1)//删掉awa
{
if(t==0) get_end();
if(t==0) break;
dt[e[t].x][e[t].y]=WALL;
gotoxy(e[t].y-1,e[t].x-1);//没错,我当时把这一行放在了t--下面调了1个小时
t--;
printf(" ");
Sleep(1);
}
}
void tian()//填充颜色&&判断是否合法
{
int sx=0,sy=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(dt[i][j]==KONG) sx=i,sy=j;
queue<play_er> q;
while(!q.empty()) q.pop();
bool vis[1010][1010];
memset(vis,0,sizeof vis);
vis[sx][sy]=1;
q.push((play_er){sx,sy});
while(!q.empty())
{
play_er p=q.front();
gotoxy(p.y-1,p.x-1);
color(15);
printf("#");Sleep(debug);
q.pop();
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(!vis[X][Y]&&dt[X][Y]==KONG)
{
q.push((play_er){X,Y});
vis[X][Y]=1;
}
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(dt[i][j]==KONG&&!vis[i][j])
{
MessageBox(NULL,"你的阳寿不够,即将重新设定地图","wrong",MB_ICONEXCLAMATION|MB_OK);
D=1;
return;
}
}
print();
}
void del()
{
tian();//填充+广搜判断是否全联通
if(D) return;
try__();
print();
}
int main()
{
noedit();
srand(time(NULL));
full();
hide();
gotoxy(0,cy-2);
printf("(回车启动,Esc退出)");
while(1)
{
if(kd(VK_ESCAPE)) break;
gotoxy(0,cy-2);
printf("(回车启动,Esc退出)");
if(kd(VK_RETURN)) cls,try_(),make_way(),connect(),del();
if(D)
{
D=0;
cls;
try_(),make_way(),connect(),del();
}
Sleep(30);
}
return 0;
}
3. 实用
//地图生成,是以外放txt的形式,但可能出现格式错误,用devc++打开即可解决
#include<conio.h>
#include<windows.h>
#include<bits/stdc++.h>
using namespace std;
namespace init_ //小技巧15
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME)&0x8000)?1:0)
#define sl(n) Sleep(n)
#define cls system("cls")
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init_;
namespace Color//加上颜色更加美观
{
void color(int ForgC)
{
int BackC=0;
WORD wColor=((BackC&0x0F)<<4)+(ForgC&0x0F);
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),wColor);
}
}
using namespace Color;
#define cx 240
#define cy 65
#define m 239
#define n 61
#define WALL -1
#define KONG 2
bool D=0;
struct room_{int x,y,lx,ly;}rm[150];
struct play_er{int x,y;}start,e[1001000];
int t,dt[1010][1010],fx[5][5]={{2,0},{-2,0},{0,2},{0,-2}},zj[5][5]={{1,0},{-1,0},{0,1},{0,-1}};
bool v[1010][1010]={0};
void try_();//插入房间
void dfs(int,int);//造迷宫
void make_way();//地图初始化
void connect();//连接房间与路
void get_end();//取出死路
void try__();//删路
void tian();//判断合法
void del();//删路总调控
void optimize();//地图优化
void make_dt();//造地图
int main();//main函数
void try_()//插房间,为了方便,房间的x,y坐标为偶数,长宽为奇数
{
memset(rm,0,sizeof rm);t=0;
for(int i=1;i<=1000;i++)
{
int x=rand()%cx,y=rand()%(cy-3),lx=rand()%11+20,ly=rand()%6+7;
bool f=1;
if(x%2!=0) x++;
if(y%2!=0) y++;
if(lx%2==0) lx++;
if(ly%2==0) ly++;
if(x+lx>=cx||y+ly>=cy-3||x==0||y==0) f=0;
for(int j=1;j<=t&&f;j++)
{
room_ r=rm[j];
if((max(x,r.x)<=min(r.x+r.lx,x+lx))&&(max(-r.y-r.ly,-y-ly)<=min(-r.y,-y))) f=0;
}
if(f) rm[++t]=(room_){x,y,lx,ly};
}
}
void dfs(int x,int y)//小技巧6
{
bool f[5]={0};
while(1)
{
if(f[0]&&f[1]&&f[2]&&f[3]) return ;
int r=rand()%4;
int nx=fx[r][0],ny=fx[r][1],zx=zj[r][0],zy=zj[r][1];
if(x+nx<1||x+nx>n||y+ny<1||y+ny>m) {f[r]=1;continue;}
if(dt[x+zx][y+zy]!=WALL||v[x+zx][y+zy]||v[x+nx][y+ny]) {f[r]=1;continue;}
f[r]=1;
dt[x+zx][y+zy]=KONG;
v[x+zx][y+zy]=v[x+nx][y+ny]=1;
dfs(x+nx,y+ny);
}
return ;
}
void make_way()
{
memset(v,0,sizeof v);//一般的初始化
memset(dt,WALL,sizeof dt);
start.x=2,start.y=2;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(i==1||i==n||j==1||j==m) dt[i][j]=WALL;
if(i%2==0&&j%2==0) dt[i][j]=KONG;
else dt[i][j]=WALL;
}
for(int i=1;i<=t;i++)for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)//标记房间
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++) dt[y][x]=KONG,v[y][x]=1;
for(int i=0;i<=n;i++)for(int j=1;j<=m;j++)//建路
if(dt[i][j]==KONG&&dt[i-1][j]==WALL&&dt[i+1][j]==WALL&&dt[i][j-1]==WALL&&dt[i][j+1]==WALL) dfs(i,j);
}
void connect()//连接路与房间
{
const int ch=3;//3%的概率选中
int sum=0;
for(int i=1;i<=t;i++)//枚举房间,分别枚举上面的边,左边的边,下面的边和右边的边
{
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++)//先找能与之相连的总数
{
if(dt[y][rm[i].x-2]==KONG) sum++;
if(dt[y][rm[i].x+rm[i].lx+1]==KONG) sum++;
}
for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)
{
if(dt[rm[i].y-2][x]==KONG) sum++;
if(dt[rm[i].y+rm[i].ly+1][x]==KONG) sum++;
}
bool b=0;//暴力枚举连边
while(!b)
{
for(int y=rm[i].y;y<=rm[i].y+rm[i].ly-1;y++)
{
if(dt[y][rm[i].x-2]==KONG){if(rand()%100<ch||sum==1){dt[y][rm[i].x-1]=KONG,b=1,sum--;continue;}sum--;}
if(dt[y][rm[i].x+rm[i].lx+1]==KONG){if(rand()%100<ch||sum==1){dt[y][rm[i].x+rm[i].lx]=KONG,b=1,sum--;continue;}sum--;}
}
for(int x=rm[i].x;x<=rm[i].x+rm[i].lx-1;x++)
{
if(dt[rm[i].y-2][x]==KONG){if(rand()%100<ch||sum==1){dt[rm[i].y-1][x]=KONG,b=1,sum--;continue;}sum--;}
if(dt[rm[i].y+rm[i].ly+1][x]==KONG){if(rand()%100<ch||sum==1){dt[rm[i].y+rm[i].ly][x]=KONG,b=1,sum--;continue;}sum--;}
}
}
}
return;
}
void get_end()//取出死路位置,但注意之前的代码有bug,现在改了
{
int sx=0,sy=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(dt[i][j]==KONG)sx=i,sy=j;
queue<play_er> q;
while(!q.empty()) q.pop();
bool vis[1010][1010];
memset(vis,0,sizeof vis);
q.push((play_er){sx,sy});
vis[sx][sy]=1;
while(!q.empty())
{
play_er p=q.front();
q.pop();
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(!vis[X][Y]&&dt[X][Y]==KONG) q.push((play_er){X,Y}),vis[X][Y]=1;
}
int f=4;//删路
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(dt[X][Y]==KONG) f--;
}
if(f==3) e[++t]=p;
}
}
void try__()//反复删除
{
get_end();
while(1)//删掉awa
{
if(t==0) get_end();
if(t==0) break;
dt[e[t].x][e[t].y]=WALL;
t--;
}
}
void tian()//填充颜色&&判断是否合法
{
int sx=0,sy=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(dt[i][j]==KONG)sx=i,sy=j;
queue<play_er> q;
while(!q.empty()) q.pop();
bool vis[1010][1010];
memset(vis,0,sizeof vis);
vis[sx][sy]=1;
q.push((play_er){sx,sy});
while(!q.empty())
{
play_er p=q.front();
q.pop();
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(!vis[X][Y]&&dt[X][Y]==KONG)q.push((play_er){X,Y}),vis[X][Y]=1;
}
}
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(dt[i][j]==KONG&&!vis[i][j]){D=1;return;}
}
void del()
{
tian();//填充+广搜判断是否全联通
if(D) return;
try__();
}
void optimize()
{
int sx=0,sy=0;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(dt[i][j]==KONG)sx=i,sy=j;
queue<play_er> q;
while(!q.empty()) q.pop();
bool vis[1010][1010],v2[1010][1010];
memset(vis,0,sizeof vis);
memset(v2,0,sizeof v2);
vis[sx][sy]=1;
q.push((play_er){sx,sy});
while(!q.empty())
{
play_er p=q.front();
q.pop();
for(int i=0;i<4;i++)
{
int X=p.x+zj[i][0],Y=p.y+zj[i][1];
if(!vis[X][Y]&&dt[X][Y]==KONG)q.push((play_er){X,Y}),vis[X][Y]=1;
}
}
int fx[]={0,-1,-1,-1,0,0,1,1,1},fy[]={0,-1,0,1,-1,1,-1,0,1};
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(vis[i][j])for(int k=0;k<=8;k++)v2[i+fx[k]][j+fy[k]]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(!v2[i][j])dt[i][j]=KONG;
}
void make_dt()
{
zzb:
try_(),make_way(),connect(),del();
if(D)
{
D=0;
goto zzb;
}
optimize();
freopen("mp.txt","w",stdout);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) printf("%c",dt[i][j]==WALL?'#':' ');
puts("");
}
}
int main()
{
srand(time(NULL));
hide();
noedit();
make_dt();
return 0;
}
你可以把它当头文件生成mp后在读入
也可以直接用数组dt当地图用
注意一点:
dt[i][j]==KONG 代表是可经过的路
dt[i][j]==WALL 代表的是不可经过的墙(直接用KONG和WALL,毕竟KONG是2,WALL是-1···)
3. 按钮
字面意思,创造一个能被点击的按钮。
dev 没有直接提供这个功能,所以这里的实现只是一种模拟。
对于一片区域,我们假定它为按钮时,需要做到以下几点:
- 鼠标移动在上面时给个显示吧(这个可以不要)
- 点击时给个反应吧。
所以整体就很好想。框定一片区域为按钮后,不断查询鼠标位置。如果鼠标在这个区域上点击,即视为按下按钮。
拆分之后,代码的部分也很好思考。
我们需要:
- 判断是否按下鼠标,用 kd 就能解决。
- 求鼠标点击时的相对位置,windows.h 有这个功能。
- 移动按钮至指定位置,用 gotoxy 就能解决。
代码用的是之前老版本,有一个问题:只能维护中文按钮。
如果你想维护英文按钮的话,可以传参的时候把两个字符压成一个。
其中需要注意,中文按钮的 gotoxy 中,因为前文提到一个中文占了两个字符,所以需要乘二。在判断鼠标位置的时候,也做了对应处理。所以如果你想改成英文按钮的话,这几个地方都要修改。
字数太多会导致网页卡顿,所以代码就不放了,文章开头文件夹中应该有。
4. 多线程
从这里开始上难度了。但是其实不用完全掌握,过个脑子就行,具体的栗子在后面会举。
其实写写就会了。
这篇的由来是解决之前的一个问题:
请问:
大佬能解释一下怎么同时运行两个c++for循环吗? 就比如说游戏里你一边出招电脑也能出招这种的
如果你仔细看了前面的内容,用 kd 就能解决。
但归根结底,这并不是同时运行两个 for (虽然多线程从理论上讲也不是)。
这边我们介绍一个最为常用的多线程:thread。
1. 定义
c++11中,头文件
#include<thread>
定义了 thread 类。创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);
只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。
这个看这很复杂,打个不恰当的比方,其实就是类似于 sort 你要自己写 operator 一样。
举个栗子:
#include<iostream>
#include<thread>
using namespace std;
void zzb(int a){cout<<"这是线程"<<a<<"\n";}
int main()
{
cout<<"主线程:"<<endl;
//定义 线程名 函数名 参数
thread th2 (zzb, 9);
cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id
th2.join();//暂停主线程,运行th2
return 0;
}
大家可以自己运行一下。
除了直接创建,匿名函数lambda(其实就是函数),class 也可以。
不过通常情况都是建立函数来调用。
2. 成员函数
| 函数名 | 作用 |
|---|---|
| get_id | 获取线程 ID |
| joinable(bool) | 检查线程是否可被 join,如果线程未被join或detach则返回true |
| join | 阻塞(暂停)当前线程直到join的线程返回 |
| detach | 不阻塞当前线程,不等待该线程返回,相当于这是个守护线程。 |
| swap | 交换线程 |
关于一些名词,比如阻塞这些,给大家举个例子方便理解。
现在有三个 task,其中三个 task 都需要先运行一段时间。完成 task 1 需要 task 2,3 完成时返回的结果。
你在完成 task 1,中途,你让 z_z_b_ 帮你运行 task 2(创建线程 1 ),让你的同桌帮你完成 task 3(创建线程 2)。
现在你完成了 task1,只需要等 z_z_b_ 和你的同桌完成 task 23 ,用到它们的结果即可,所以需要 join(),堵塞主线程,等他们完成任务(子线程运行结束),即可继续运行主线程。
3. 互斥量
如果你运行了第一个程序,你就会发现其实生成的结果并不一定是完全一样的。
而对于这个 bug,用互斥量(lock)锁住就能解决。
给一个网上找到的栗子:
单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用。
此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock)。
那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。
互斥量在 #include<mutex> 中。
给大家举一个用法:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void p1(int a)
{
m.lock();//许可
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
m.unlock();//归还,一定要写,不然TLE
}
void p2(int a)
{
m.lock();
cout<<"p2函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
m.unlock();
}
void p3(int a)
{
cout<<"p3函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}
void p4(int a)
{
cout<<"p4函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
system("pause");
thread pr3(p3,a);
thread pr4(p4,a);
pr3.join();
pr4.join();
return 0;
}
前面是使用互斥量的结果,后面是不使用的结果。
效果不明显可以多运行几次。
1. lock()
调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
-
如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
-
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
-
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)(即 TLE)。
2. unlock()
解锁。注意有锁才解锁哦。
3.try_lock()
字面意思,尝试上锁,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数时也会出现三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
4. lock_guard()
一个偷懒的局部对象,效果是对象内自动上锁解锁。
这个举个例子:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void p1(int a)
{
lock_guard<mutex> g1(m);//用此语句替换了m.lock();
//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
void p2(int a)
{
{
lock_guard<mutex> g2(m);
cout<<"p2函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
cout<<"作用域外的内容3"<<endl;
cout<<"作用域外的内容4"<<endl;
cout<<"作用域外的内容5"<<endl;
}
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
return 0;
}
当然,lock_guard()传双参时,如果第二个为 adopt_lock 标识,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。
如:
void p1(int a)
{
m.lock();
lock_guard<mutex> g1(m,adopt_lock);//用此语句替换了m.lock();
//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
5. unique_lock()
这个比上一条更强,支持了更多的操作。除了adopt_lock,它还支持:
- try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
- defer_lock: 始化了一个没有加锁的mutex。
栗子:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void p1(int a)
{
unique_lock<mutex> g1(m,defer_lock);//始化了一个**没有加锁**的mutex
cout<<"关注一下吧"<<endl;
g1.lock();//手动加锁;
//注意,不是m.lock();
//注意,不是m.lock();
//注意,不是m.lock()
cout<<"proc1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
g1.unlock();//临时解锁
cout<<"祝关注的谷友"<<endl;
g1.lock();
cout<<"NOIP比我考得高"<<endl;
}//自动解锁
void p2(int a)
{
unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
cout<<"proc2函数正在改写a" << endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}//自动解锁
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
return 0;
}
初次之外还有一些其他类型的互斥量:
6. recursive_mutex
允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权
放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同
其余=mutex
7. time_mutex
在 mutex 的基础上增加了:
-
try_lock_for
函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false)
如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
-
try_lock_until
函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
#include<iostream>
#include<chrono>
#include<thread>
#include<mutex>
using namespace std;
timed_mutex m;
void p1()
{
//等lock: 每随机ms输出一次喵喵?
while(!m.try_lock_for(std::chrono::milliseconds(rand()%200+1))) cout<<"喵喵?";
//得到lock,等1s后输出喵!
this_thread::sleep_for(std::chrono::milliseconds(1000));
cout<<"喵!";
m.unlock();
}
int main()
{
srand(time(NULL));
std::thread t[5];//线程组
for(int i=0;i<5;i++)t[i]=thread(p1);
for(auto& th:t)th.join();
return 0;
}
8. recursive_timed_mutex
6 和 7 的融合版。不谈了不讲了不解释了下一条。
9. condition_variable
这个东西类比一下就是 scratch 的广播。
举个例子:
你在爆切比赛,发现题切完了。这个时候你就联系出题人在出几道(join)
出题人收到之后爆肝出来@了你(收到广播),然后你就可以继续操作了。
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<thread>
#include<mutex>
#include<bits/stdc++.h>
using namespace std;
int a=0;
mutex m;
condition_variable cond;
void th1()
{
unique_lock<mutex> lock(m);//自动上锁,解放双手
cond.wait(lock,[]{return!(a%1000);});//回应
++a;
}
void th2()
{
for(int i=0;i<100000;++i)
{
unique_lock<std::mutex> lock(m);
if(!(a%1000)) cond.notify_one();//回应
++a;
}
}
int main()
{
thread t1(th1);
thread t2(th2);
t1.join();
t2.join();
cout<<a<<endl;
return 0;
}
或者举一个更有意思的:
#include<iostream>
#include<deque>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<windows.h>
using namespace std;
deque<int> q;
mutex m;
condition_variable cond;
int c=0;//缓冲区的钱个数
void p()
{
int d;
while(1)
{
//通过外层循环,能保证生成用不停止
if(c<3)
{//限流
{
d=rand();
unique_lock<mutex> locker(m);//锁
q.push_front(d);
cout<<"存了"<<d<<"元"<<endl;
cond.notify_one();//通知取
++c;
}
Sleep(500);
}
}
}
void co()
{
int d;
while(1)
{
{
unique_lock<mutex> locker(m);
while(q.empty()) cond.wait(locker); //wati()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
d=q.back();//取的第一步
q.pop_back();//取的第二步
cout<<"取了"<<d<<"元"<<endl;
--c;
}
Sleep(1500);
}
}
int main()
{
thread t1(p);
thread t2(co);
t1.join();
t2.join();
return 0;
}
10. 原子结构
有点像 struct。他不能强制停止,不能交换,同时不需要上锁。
相当于简单数据结构(int,bool,char)上了层保护说是。
此事在 #include<atomic> 中有记载。
如果你的变量在线程中需要改变,这边建议亲用这个。
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<thread>
#include<atomic>//原子
using namespace std;
atomic<int> a;
//int a=0;//你可以试一下,与期望答案不一样oh
void th1()
{
for(int i=0;i<100000;i++) ++a;
}
void th2()
{
for(int i=0;i<100000;i++) ++a;
}
int main()
{
thread t1(th1);
thread t2(th2);
t1.join();
t2.join();
cout<<a<<endl;
return 0;
}
大概就这么多。如果有兴趣也可以看看 async,不过那玩意虽然比这玩意泛用但我感觉更难理解。
5. 局域网联机
最后一个,也是最难但是最有意思的一个。
同样的,过个脑子,下面是有例子的。
但是会这个的前提其实是会多线程喵,不过无所谓,里面有例子可以慢慢看。
1.原理
举个例子:你手中有一个二座插头,面前有许多插座,一个二孔插头和一堆三孔插头
你会把它插到哪里?
很明显,其实都行是那个二孔插座
转换一下:
你手中那的是一个叫端口的东西,一段插在服务器(不确定叫啥鬼名字,就干脆总端叫服务器,链接的叫客户端)上。
而你接下来就是要把端口插到合适的位置,使电流能够从那个位置流向服务器再流回来
就是把信息带给服务器,以服务器为中控,分发到每一个小的客户端上。
再举个例子:
就是有一堆点,都连向同一个虚点。
而其中的某个点想发出信息时,只能把信息传输到虚点再传给其他所有点。
2.Winsock
在头文件中某个不为人知的角落中,藏有一个神秘的头文件
#include<Winsock2.h>
但是,注意一点:这个头文件最好写在 windows.h 的前面。
因为 WinSock2.h 重定义了旧版本的 Winsock,而 Winsock 被包含在了 Windows.h 中。
它就是 dev 封装的,连接系统和用户使用的软件之间用于交流的一个接口
当然,它之中包含了链接库函数,需要
#pragma comment (lib,"ws2_32")
来进行各种函数的调用。
还要在编译命令(工具[T]->编译选项[C])中加上
-lws2_32
在这个头文件中,常见(其实都不常见)的其实不多。
3.WSADATA
struct WSAData//函数原型
{
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYSSTATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
};
大概里面成员的作用也比较简单
| 成员 | 作用 |
|---|---|
| wVersion | 存储Winsockets规范的版本号 |
| wHighVersion | 与上面没啥区别 |
| szDescription | Windows Sockets实现的描述拷贝到这个字符串中 |
| szSystemStatus | ↑ 把有关的状态等信息拷贝到该字符串中 |
| iMaxSochets | 单个进程能够打开的socket的最大数目 |
| iMaxUdpDg | 表示数据报的最大长度,现在被废弃了 |
| lpVendowInfo | 为Winsock实现而保留的制造商信息,现在被废弃了 |
它通常拿来存储被 WSAStartup 函数调用后返回的 Windows Sockets 数据。它包含 Winsock.dll 执行的数据。
你也看到了,上面说的是 WSAStartup 调用后返回的数据
所以 WSAStartup 很明显是必须要用的
使用Socket(一会讲解)的程序在使用Socket之前必须调用WSAStartup函数。
这样应用程序才可以调用所请求的Socket库中的其它Socket函数了。
至于使用吗,就是个板子
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//这个表示调用失败
//其中MAKEWORD表示socket的版本,这里就是2.2版本
//就用2.2就行了,不要乱改
当然,它既然是一把打开Socket库的钥匙,那我们用完了就不能留下来。
WSACleanup();
WSACleanup 可以释放它所占用的系统资源。
这样子,我们才算打开了Socket库。
还有一点注意的是,使用了多少次 startup 就要用多少次 cleanup
4.Socket
Socket,端口,片面理解即是链接服务器与客户端中的线路。
所以,我们就能简单的描述一下服务器与客户端之间的关系了
5.客户服务器模式
更详细的自己去百度百科看。下面保留了一些容易理解的。
服务器:
- 打开一通信通道并告知本地主机,它愿意在某一公认地址上接收客户请求
- 等待客户请求到达该端口
- 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止
- 返回第二步,等待另一客户请求。
- 关闭服务器
客户方:
- 打开一通信通道,并连接到服务器所在主机的特定端口
- 向服务器发请求,等待并接收应答;然后继续提出请求
- 请求结束后关闭通信通道并终止
看起来很高级,不过用通俗一点的方式就是:
A 把信息传输到服务器中
而 B,C,D,E 则收到从服务器传来的信息
同样,B、C、D、E 也可以发出消息让 A 来收
服务器它就是一个中转站而已。
6.函数
1. socket
SOCKET PASCAL FAR socket(int af,int type,int protocol);
这个是 socket 的初始化,af是协议族,就是从传输协议( AF_UNIX、AF_INET、AF_NS等 )中选择一个。常见的是 AF_INET,用这个就行了。
而 type 指明了 socket 发送与分组的方式。建议使用 SOCK_STREAM (TCP流式),这个相对来说面向连接、可靠,数据无差错、无重复,且按发送顺序接收。(一句话,就这个最好用)
最后的 protocol 表示发送数据时应使用的协议,值为0时表示用type默认的形式。建议还是用 TCP。
直接给一个板子,用的是 ipv4+TCP
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//其中server是SOCKET类型。
注意,同那啥 WSAStartup 一样,用了也要释放资源。
closesocket(SOCKET sock);
2.bind与listen
blind,绑定。
int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR*name,int namelen);
当然,绑定中要用到 sockaddr,我们接下来先介绍 sockaddr,再回来介绍 bind
listen,监听。
int listen(
SOCKET sock,
int backlog//队列中允许传入的最大连接数,达到最大值后后续连接将会被丢弃
);
它成功时返回0,错误时返回-1
3.sockaddr与sockaddr_in
每一个网络层数据包都需要一个源地址和一个目的地址,如果数据包封装传输层数据,还需要一个源端口和一个目的端口。
为了将地址信息传入和传出socket库,API提供了sockaddr数据类型
struct sockaddr
{
uint16_t sa_family;//常数,指定地址类型,应与创建socket时使用的参数af一致
char sa_data[14];//存储地址
};
为了更加方便,它还封装了一个更厉害的 sockaddr_in
struct sockaddr_in
{
short sin_family;//和sockaddr中的sa_family具有相同含义
uint16_t sin_port;//存储地址中的16位端口部分
struct in_addr sin_addr;//存储4字节的IPv4地址
char sin_zero[8];//不使用,仅为了使sockaddr_in与sockaddr的大小一致,应全为0
};
所以,用这个来暂时转换变量后,bind 也可以用了
sockaddr_in bindser;//绑定本地地址
bindser.sin_family=AF_INET;
bindser.sin_port=htons(port);
bindser.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0)){WSACleanup();closesocket(server);return 0;}//如果有服务器存在了
然后接下来,该处理接入的问题了
4.accept与connect
accept ,接收,服务器接收客户端
int connect(
SOCKET sock,//待连接的socket
const sockaddr*addr,//指向目的远程主机的地址指针
int addrlen//addr参数所指向地址的长度
);
connect ,连接,客户端连接服务器
SOCKET accept(
SOCKET sock,//接收传入连接的监听socket
sockaddr*addr,//将会被写入请求连接的远程主机地址,不需要初始化
int* addrlen//指向addr缓冲区大小的指针,以字节为单位,真正写入地址之后将更新这个参数
);
用法就是填空awa
然后最后,就是最重要的两个:
5.send与recv
send是发消息,recv是接收消息。
int send(
SOCKET sock,//端口
const char*buf,//要发送的字符串
int len,//字符串长度
int flags//传输方式
);
如果send成功,返回发送数据的大小。如果socket的输出缓冲区有一些空余的空间,但不足以容纳整个buf时,这个值可能会比参数len小。如果没有空间,默认情况下,调用线程将被阻塞,直到调用超时或者发送了足够的数据后产生空间。如果发生错误,send函数返回-1。请注意,非零的返回值并不代表数据已经成功发送出去了,只能说明数据被存入队列中等待发送。
而recv也差不多
int recv(
SOCKET sock,//端口
char* buf,//要接收的字符串
int len,//要接收的长度
int flags//传输方式
);
常用的就这些,所以我们就讲这么多。
那么接下来,工具到手了,还要看你会不会用。
7.客户服务器模式实现
在这里,给一个我认为写的不错的实现代码
科技系列 - 2 < 更新第二期 >:DEV - C++ 不同主机下的同局域网联机 更新 第 2 期https://blog.csdn.net/Hox_5/article/details/120355064
这个在我看来还是不错的,不过还是有一些可以改进的地方。
所以,我们来看看它的不足在哪里。
- 客户端与服务器分开放置。
- 服务器产生的运行框有点碍眼
其实有点挑刺了(?,不过这样修改之后个人感觉还是挺顺眼的。
强迫症说是。
- 利用 bind ,我们能判断是否已经有服务器存在。如果没有,就把当前程序改造成服务器,反之,就改造成客户端。
- 如果它被改造成服务器,隐藏到后台,然后重新再创造一个运行框出来
不过,这样也有一个问题,就是:你怎么把后台的服务器调出来关掉?
首先,很明显,当我们打开服务器时,里面至少要有一个客户端存在才行对吧,毕竟你当前的电脑运行着一个服务器和一个客户端,别人再加进来客户端肯定会增加。
所以,如果客户端的数量为 0 了,就是没有人在使用了,服务器自然没用了,直接 exit(0);
再注意一点:发送消息和接收消息最好用多线程。
8.模板 - Server
模板不全,请注意
Server 为了方便大家,我封装了一下。
给大家说明一下:
z_z_b_ 的 Server 会发送 3 中消息
- Somebody加入了服务器
- Somebody离开了
- Somebody:A B C D,,,E
其中,Somebody 是用户名,而 A B C D 建议你传数字,用 int_to_string 转换,atoi 接收就行了
Server 的板子就不用大家改了,但需要注意:
Server只是中间商还不要差价的那种
所以为了代码的美观,不要在中间商那里放置一些奇奇怪怪的东西好不好
(zzb 的意思是:你游戏中的过程那些不要写进 Server 里面,不然真的很乱很乱)
namespace Server
{
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET 用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,size,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
puts("端口已打开!\n");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,size,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
(等一下,好像还有几个需要提醒的东西:)
- 注意,zzb 用的是 namespace 封装,所以一定注意,不要 using 了它
- 用法:
namespace Server
{
//略
}
//不要using namespace Server;
int main()
{
bool f=0;
Server::main(f);//这样使用
//f=1时表示已经存在服务器了,f=0时表示还没有服务器,会直接进入Server中的造服务器阶段,不会再返回到main这里
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
}
9.模板 - 客户端
模板不全,请注意
这个也是,zzb 已经帮你划分好了哪些地方是要填空的,直接填空就行了
(不要想的太复杂,其实就是把你游戏中P1 while(1) 的操作搬进来,然后其他玩家的操作就是recv之后拆解信息,直接更改就行了)
const int N=20,P=26;
SOCKET server;
char a[1000],ipv4[1000];
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);//拆分一个个数字
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
//拆解的信息留在v里,s是用户名,最好hash一下用下标存放相应信息
//一定注意:v中的信息下标从0开始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作,操作后ms=1
//发送所有信息,信息就是你要发送的数字,中间用空格隔开,转换为char*类
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
if(strlen(a)>=20)
{
printf("输入用户名过长\n\n");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip\n");
gets(ipv4);
puts("");
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
10.总模板
三份模板最好都看看,前两个看注释,最后一个看代码
注释一定要看!!!一些很重要的都在注释里面
//编译命令(工具[T]->编译选项[C])加上-std=c++17 -lws2_32
#include<bits/stdc++.h>//win+r 输入cmd后 输入ipconfig,ipv4后面的就是你的ip
#include<Winsock2.h>
#include<windows.h>
#include<queue>
#pragma comment (lib,"ws2_32")
#define port 9999//端口号
#define sz 6400//缓冲区长度
#define people 10//限制人数
using namespace std;
namespace init
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000)?1:0)
#define sl(n) Sleep(n)
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init;
namespace Server
{
bool Hi=0;
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,sz,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI whide(LPVOID param)//隐藏窗口,这个很爽真的
{
HWND hWnd=GetConsoleWindow();
while(1)
{
if(kd(VK_ESCAPE))
{
if(Hi) Hi=0;
else Hi=1;
}
if(Hi) ShowWindow(hWnd,SW_HIDE);
else ShowWindow(hWnd,SW_SHOW);
Sleep(100);
}
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
CreateThread(NULL,0,whide,NULL,0,NULL);//开线程
puts("端口已打开!\n按一次ESC隐藏,按第二次唤起");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,sz,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
const int P=26;
SOCKET server;
char a[1000],ipv4[1000];
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作
//发送所有信息
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
gets(a);
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
auto check=[](string s)
{
for(int i=0;i<s.size();i++)
if(s[i]=='\0'||s[i]=='\n'||s[i]<0) return 0;
return s.size()>0?1:0;
};
if(strlen(a)>=20||check(a)==0)
{
printf("输入用户名违规\n\n");
Sleep(1000);
system("cls");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip(输入return返回上一步)\n");
gets(ipv4);
puts("");
if(!strncmp("return",ipv4,6))
{
system("cls");
goto set_name;
exit(0);
}
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
int main()
{
system("mode con cols=120 lines=30");
noedit();
hide();
bool f=0,F=0;
puts("请选择:是否已经启动了服务器\n0:否\n1.是");
cin>>f;
F=f;
system("cls");
if(!f) Server::main(f);//调用Server里面的main,其中有个保险,但是只能查询本地是否有服务器
if(F!=f)
{
printf("本地已有服务器存在,将更换为客户端\n(按 空格 继续)");
while(1)if(kd(VK_SPACE))break;
}
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
return 0;
}
这份代码已经很全了,但是还有一个 z_zb 没解决的问题:
有时候传用户名的时候会失败,不知道为什么
所以,在组队的时候注意一下:
一定要打开服务器看一下用户名传起没,不然你收不到对方的消息qwq
好像 csdn 有人回答了这个问题的,不过我懒得改了。
还有一点
客户端应该输入选择的服务器的IP,客户端应该输入选择的服务器的IP,客户端应该输入选择的服务器的IP
你选择的是哪个服务器的IP,你就进的那个服务器。
不知道 IP 的自己开 cmd 查询。
最后,给一个实例
11.实例
本来想写个联机走迷宫的,但为了赶进度就抛弃它了
这个联机再挖个坑(反正够多了,也不差这一个),zzb 会尽力把它用到 地牢探索/元气骑士? 里面(愿脏脏包之神保佑我)
所以只写了一个只能移动的代码
实例说明:
选择本机房任意一台电脑,运行服务器。(建议不要隐藏,如果有人用户名传输失败还能看见)
WSAD移动
//编译命令(工具[T]->编译选项[C])加上-std=c++17 -lws2_32
#include<bits/stdc++.h>//win+r 输入cmd后 输入ipconfig,ipv4后面的就是你的ip
#include<Winsock2.h>
#include<windows.h>
#include<queue>
#pragma comment (lib,"ws2_32")
#define port 9999//端口号
#define sz 6400//缓冲区长度
#define people 10//限制人数
using namespace std;
namespace init
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000)?1:0)
#define sl(n) Sleep(n)
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init;
namespace Server
{
bool Hi=0;
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,sz,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI whide(LPVOID param)//隐藏窗口,这个很爽真的
{
HWND hWnd=GetConsoleWindow();
while(1)
{
if(kd(VK_ESCAPE))
{
if(Hi) Hi=0;
else Hi=1;
}
if(Hi) ShowWindow(hWnd,SW_HIDE);
else ShowWindow(hWnd,SW_SHOW);
Sleep(100);
}
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
CreateThread(NULL,0,whide,NULL,0,NULL);//开线程
puts("端口已打开!\n按一次ESC隐藏,按第二次唤起");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,sz,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
const int N=30,M=119,P=26;
SOCKET server;
char a[1000],ipv4[1000];
int top=1;
map<string,int> mp;
struct player{int x,y;void check(){x=max(1,x),x=min(M-2,x),y=max(1,y),y=min(N-2,y);}}p[P];
void sent()
{
for(int i=1;i<=M;i++) printf("#");
puts("");
for(int i=2;i<N;i++)
{
printf("#");
for(int j=2;j<M;j++) printf(" ");
printf("#");
puts("");
}
for(int i=1;i<=M;i++) printf("#");
}
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
if(!mp[s]) mp[s]=++top;
if(p[mp[s]].x!=0) gotoxy(p[mp[s]].x,p[mp[s]].y);
printf(" ");
p[mp[s]].x=v[0];
p[mp[s]].y=v[1];
p[mp[s]].check();
gotoxy(p[mp[s]].x,p[mp[s]].y);
printf("%c",(mp[s]-1+'A'));
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作
if(kd('W'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].y--,ms=1;
}
if(kd('S'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].y++,ms=1;
}
if(kd('A'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].x--,ms=1;
}
if(kd('D'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].x++,ms=1;
}
p[1].check();
//发送所有信息
if(ms) gotoxy(p[1].x,p[1].y),printf("A");
sendn=to_string(p[1].x)+" "+to_string(p[1].y)+" ";
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
gets(a);
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
auto check=[](string s)
{
for(int i=0;i<s.size();i++)
if(s[i]=='\0'||s[i]=='\n'||s[i]<0) return 0;
return s.size()>0?1:0;
};
if(strlen(a)>=20||check(a)==0)
{
printf("输入用户名违规\n\n");
Sleep(1000);
system("cls");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip(输入return返回上一步)\n");
gets(ipv4);
puts("");
if(!strncmp("return",ipv4,6))
{
system("cls");
goto set_name;
exit(0);
}
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
sent();
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
int main()
{
system("mode con cols=120 lines=30");
noedit();
hide();
bool f=0,F=0;
puts("请选择:是否已经启动了服务器\n0:否\n1.是");
cin>>f;
F=f;
system("cls");
if(!f) Server::main(f);//调用Server里面的main,其中有个保险,但是只能查询本地是否有服务器
if(F!=f)
{
printf("本地已有服务器存在,将更换为客户端\n(按 空格 继续)");
while(1)if(kd(VK_SPACE))break;
}
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
return 0;
}
4. 一点实例
这些其实有部分是 z_z_b_ 鸽掉的东西。也不打算更了。部分有一定可玩性。
还有部分是对于一些常见套路的讲解。
大部分其实看一下我写的代码就可以了,不懂欢迎问。
别私信问,z_z_b_ 强迫症删私信,发太多会急。
1. 实例-恶魔轮盘赌
这个完成的差不多了。
目前有一个 bug 和一个待优化的地方。
bug:
不知道为什么,装填子弹会出现问题,不知道是哪里写错了。之前查半天也没查出来。
不过除了影响手感以外不影响游戏平衡(好像是吧)
待优化:
想写成局域网联机的。可惜退役了。
2. 实例-动物园怪谈
差玩法。里面我的页面这些完成的差不多了。
其实里面是有隐藏的道路的awa。
里面每一个目标点都应该对应一个任务,但是来不及写了,太多了不想写其实是。
还没包括隐藏任务的。
这里提一嘴吧,如果有好人想帮 z_z_b_ 补完,完成所有普通任务解锁普通的结局。
这个时候所有兔子都会朝你追过来减你 san 值。
如果完成一些隐藏任务(有隐藏的任务点),解锁隐藏的结局。
也许可以打打boss之类的。
3. 实例-谁与争锋
整体完成度较高。
和朋友一起写的,只不过他好像也没写了。
哎,时间啊。
我接手的时候修改了大部分 bug,现在应该不剩什么了。
虽然没胎神的好玩,但是也还不错吧(自信)
5. 后记
其实想说的前言已经说了。
最后偷偷宣传一下。
如果这篇文章对你有帮助,麻烦点点下面的赞好吗,你谷文章排行好像是按着这个来排序的,这可以让更多的人看到。
欢迎关注 z_z_b_ 喵。
同时,团队 小说er的乐园 有很多还不错的摸鱼文,欢迎大家。
如果大家有兴趣写的话也欢迎大家加入。
新的一年,新的赛季,祝大家学业顺利,暴打 z_z_b_
closesocket(z_z_b_);
三愿百年之后的我,自如无定所
化作纷扬的尘土于万万人旁经过
就让风带着我描摹,万万物轮廓
化作你头顶星河,人间烟火眸中色