⭐零、教程概述
效果最接近《羊了个羊》(卡牌堆叠游戏)的开源代码,有数据库和关卡。
我写的程序是指 卡牌堆叠游戏 ,效果与羊了个羊一致。本教程有两个版本
PHP 使用
PHP
+
H5
+
CSS
+
JS
+
MySql
实现。
H5 使用
H5
+
CSS
+
JS
实现 。
⭐零·壹、代码获取
MieGame_C_Core 代码链接:
Github:https://github.com/MR-XieXuan/MieGame_C_Core
CandyMieGame_PHP(注意获取
V0.0.0
版本的代码):
Github: https://github.com/MR-XieXuan/CandyMieGame_PHP
CandyMieGame_H5(注意获取
V0.0.0
版本的代码):
Github: https://github.com/MR-XieXuan/CandyMieGame_H5
微信小程序版本 :
https://blog.csdn.net/apple_53792700/article/details/128041151
目录
一 、⭐逻辑的实现
♾️1.1 C语言的内核实现
逻辑实现我最先是使用 C++ 进行的,后面迁移到了 JS 。C++ 实现的牌堆代码已开源,并且注释写的也很明确,有问题可以在网站底部联系我,我很乐于回答你们的问题。
好的,回到问题。我的牌堆实现思路如下:
- 有
n
种牌 - 每
z
张相同的牌可以被抵消 - 共有
N = kz
(k属于正整数) 张牌 - 选中格有
F
格,满了即被消除 - 牌被分为 亮牌(可选) 暗牌(不可选)
- 每张牌都有唯一编号
1 - N
- 从1号牌开始放牌,大号周围
x
距离内,小号牌为暗牌 牌的大小为2x
- 所有牌都被消除即为游戏胜利
- 牌的存储形式为 (
x
,y
,t
,a
) [位置与类型与亮暗] - 没有被消除的牌被标记为1,被消除的牌被标记为0,以bitmap的形式存储
有些规则可能后面我会有所改动,但是我都会一一讲解。实在有疑问也可以联系我。
MieGame_C_Core 代码链接: https://github.com/MR-XieXuan/MieGame_C_Core
不要忘记了
Star
和
Fork
哦,还有关注和爱心。
MieGame 方法定义 :
class MieGame {
public :
// 放置一张牌
uint8_t place_a_poker(int x, int y, uint8_t type);
// 拿走一张牌
uint8_t takeaway_a_poker(int number); // 通过编号拿走
uint8_t takeaway_a_poker(int x, int y); // 通过位置拿走
// 更新亮暗状态
void refresh_poker_a();
// 获取亮暗状态
uint8_t get_poker_a(int n);
uint8_t get_poker_x(int n);
uint8_t get_poker_y(int n);
uint8_t get_poker_t(int n);
private:
int amount = 0; // 牌的总数
uint8_t pokerBitmap[63] = {0}; // 最多容纳 504 张牌
int pokerPosition[504][4] = {0}; // [牌号][位置x,位置y,类型t,亮暗a]
char Get_bit( uint8_t * st , char num ){
return ( st[(int)(num/8)] & (0x01 << (num % 8) ) ) > 0 ? 1 : 0 ;
}
void Set_bit( uint8_t * st , char num ){
st[(int)(num/8)] |= (0x01 << (num % 8) ) ;
}
void Res_bit( uint8_t * st , char num ){
st[(int)(num/8)] &= ~(0x01 << (num % 8) ) ;
}
};
♾️1.2变量以及方法介绍
♾️变量
int amount
牌的总数 , 每放置一张牌都会加一,但是拿走一张牌却不会减。
pokerBitmap[63]
bitmap 牌是否被拿走,是一个bitmap列表,被拿走为0,没有被拿走为1
pokerPosition[504][4]
牌的信息 [
牌号
][
位置x
,
位置y
,
类型t
,
亮暗a
]
place_a_poker 放一张牌
uint8_t place_a_poker(int x, int y, uint8_t type);
在指定位置放置一张指定类型的卡牌
♾️方法
takeaway_a_poker
uint8_t takeaway_a_poker(int number);
以
编号
的形式拿走一张牌,返回值是 。。。好像没有写,到时候更新。
uint8_t takeaway_a_poker(int x, int y);
以
位置
的形式拿走一张牌,比如一张牌大小为 4*4 位置在 (2,2) 则输入的是(1,1)也可以拿走这张卡牌,只要卡牌覆盖了这个位置且卡牌为亮牌。返回值为这张牌的类型,如果没有拿走牌,则返回 0 。
refresh_poker_a
void refresh_poker_a();
刷新牌堆的亮暗状态。
get_poker_x x:[a,x,y,t]
uint8_t get_poker_a(int n);
uint8_t get_poker_x(int n);
uint8_t get_poker_y(int n);
uint8_t get_poker_t(int n);
获取 编号为
n
的牌的
a(亮暗状态)
x(x位置)
y(y位置)
t(Type类型)
。
Get_bit Set_bit Res_bit
位操作,通过位操作来操作与获取bitmap的内容 ,Get是获取指定位 ,Set是把指定位变为1,Res是把指定位变为0。这是一个很简单的操作,基础不好的可以多看几遍。也不是太难理解的,我这里讲解一下
Res_bit
的实现,看懂了
Res_bit
其他的都很好理解。
void Res_bit( uint8_t * st , char num ){
st[(int)(num/8)] &= ~(0x01 << (num % 8) ) ;
}
Res_bit 需要传入两个参数,第一个是
st
bitmap的首位地址,第二个
num
是需要操作的位置。
我们假设我们的
num是12
st
传入了一个
bitmap
,数据为
0b1011111111111111
是从低位往高位读,不要读反了(从右往左)。
从右往左分析,
0x01
也就是 16进制的1,
<<
左移 ,
(num%8)
操作的位置除以8取余数也就是 4 。
0x01<<(num%8)
也就是
1<<4
也就是二进制的00010000(现在计算机最小的单位是8个字一个字节,也就是8位二进制),
~
取反 也就变成了
11101111
。
st[(int)(num/8)]
C语言强制类型转换是去掉小数部分,只保留整数部分的,也就是说(int)(12/8)就为 1 。 把
st[1] &= 0b11101111
(0b是二进制的意思,但是一般编程语言不支持直接这样子操作二进制)展开也就是
st[1] = st[1] & 0b11101111
。
也就是让
0b10111111
与
0b11101111
做并操作 得
st[1] = 0b10101111
。
最后结果就是 传入的这个bitmap从
0b1011111111111111
变成了
0b1010111111111111
第 12位变成了0。
更多关于Bitmap的可以去我的主页搜索Bitmap获取
二、⭐配置CandyMieGame并运行代码
♾️2.1.1 CandyMieGame_PHP
你需要再你的服务器中创建 Mysql 库 ,库的结构在 文件 SQL.md 中。
并且在文件index.php中填写入你访问数据库的账号与密码
$sqlname="";$sqlpassword="";
并且运行文件 SQL.md中的命令。
♾️2.1.2 CandyMieGame_H5
♾️Windows Chrome 谷歌浏览器
打开
index.html
文件后,打开
开发者工具
,并且切换到
设备仿真模式
选中iphone SE,然后刷新。
如果你的控制台输出了 :
Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. at cut_img_loding
请使用
file协议
再次打开浏览器 。
使用file协议打开浏览器的方式如下:
CMD 运行
chrome.exe --allow-file-access-from-files index.html
chrome.exe 替换成自己电脑中 chrome.exe 的路径 ,index.html 替换成自己下载好后 CandyMieGame_H5 中index.html 中的路径 。
♾️2.2.添加关卡
♾️3.2.1 CandyMieGame_PHP
在写完地图(json文件)后,把文件放在
map\level\
文件夹下并且
需要在数据库 miegame 中表 level 中添加字段 代码如下:
INSERTINTO`level`(`id`,`address`,`level`,`trytasnum`,`tasnum`)VALUES([第几关],[json存放路径],4,0,0);
♾️3.2.2 CandyMieGame_H5
在写完地图(json文件)后,把文件放在
map\level\
文件夹下
命名为 【level】 + 【第几关】+ 【.json】
修改
setup.js
文件中
window.level_amount
的值为最新的关卡数。
♾️3.运行程序
直接访问程序即可,此地址:CandyMieGame: https://game.mrxie.xyz
我服务器用的CF的CDN,所以速度较慢,且配置拉跨。首次加载时间较长。需要等待一会儿。
(PS:我经常需要加载25s左右)
电脑请切换到
设备仿真模式
iphone SE
下使用。
三、⭐JS 游戏的逻辑实现
♾️3.1MieGamePoker 对象
MieGamePoker
主要控制牌堆的处理逻辑。
该对象内的成员变量有:
MIEGAME_POKERAMOUNT_MAX
牌堆的最大容量,
POKERTYPE_AMOUNT
现牌堆有牌类型的数量,
POKER_REMOVE_NUM
已经被取走的牌的数量,
POKER_SIZE
扑克牌在地图中的边长(正方形),
amount
现牌堆有牌类型的数量,
residue
除去被被拿走的,剩余牌的数量,
pokerBitmap
牌是否被拿走的列表(bitmap),
pokerPosition
牌的信息。
该对象内的方法有:
place_a_poker
放置牌,
takeaway_a_poker
拿走牌,
refresh_poker_a
刷新牌堆亮暗信息,
get_poker_a
获取牌的亮暗信息,
get_poker_x
获取牌的x位置,
get_poker_y
获取牌的y位置,
get_poker_t
获取牌的类型,
get_poker_have
获取牌是否被拿走,
Set_bit
Bitmap置位1,
Res_bit
Bitmap置位0,
Get_bit
获取Bitmap的位。
其中外部主要用到的方法有前3个,分别为
place_a_poker
放置牌,
takeaway_a_poker
拿走牌,
refresh_poker_a
刷新牌堆亮暗信息。
具体的实现代码可以查看
miegame.js
文件。
♾️3.2MieGameCan 对象
MieGamePoker
主要控制消除区的处理逻辑。
该对象内的成员变量有:
pokerMap
现在消除区有的牌列表,
pokerAmount
现在消除区有的牌的数量。
该对象内的方法有:
place_a_poker
放置一张牌到消除区,
eliminate_pokers
对消除区的牌进行消除,
get_poker_fall
获取消除区的牌是否已经堆满。
其中外部主要用到的方法有前3个,分别为
place_a_poker
放置牌,
takeaway_a_poker
拿走牌,
refresh_poker_a
刷新牌堆亮暗信息。
具体的实现代码可以查看
miegame.js
文件。
四、⭐游戏界面层设计
♾️4.1、index.html
整个游戏都在一个
convas
画布上工作,要让这个画布充满屏幕,就应该把
body
的边框全部设为0;
<bodyonload=""onclick=""onmousemove=""style="border: 0;margin: 0;touch-action: none;"><divid="canvas_div"></div></body>
♾️miegame.index 解说
CandyMieGame 游戏界面(左图)分为 5 个区,分别为
head头
,
map地图区
,
fre释放牌区
,
props 道具区
。 因为用户设备的屏幕大小不确定,所以我们的布局也要按照用户的设备来自适应。
if(wh >=0.45&& wh <=0.5){
window.headerSize =[window.prW *0.9, window.prW *0.1];
window.headerPosition =[window.prW *0.05, window.prW *0.05+ prH *0.05];
window.mapSize =parseInt((prW)*0.9);
window.mapPosition =[parseInt((prW - mapSize)/2),parseInt((headerPosition[1]+ headerSize[1])+ prH *0.03)];
window.frePosition =[parseInt(mapPosition[0]),parseInt((mapPosition[1]+ mapSize)+ prH *0.03)];
window.freSize =[parseInt(mapSize),parseInt(prH *0.1)];
window.canPosition =[parseInt(0),parseInt((frePosition[1]+ freSize[1])+ prH *0.02)];
window.canSize =[parseInt(mapSize),parseInt(mapSize /7)];
window.canPosition[0]=(parseInt(prW - window.canSize[0])/2);
window.propsPosition =[parseInt(mapPosition[0]),parseInt((canPosition[1]+ canSize[1])+ prH *0.03)];
window.propsSize =[parseInt(mapSize),parseInt(prH *0.1)];}
在程序的最开始就按照用户操作界面的chan宽比开始分配这些区应该在哪些位置,大小应该是多少。
如下图,在长宽比不同时,各个区会以不同的方式展示在用户的屏幕上,这样子不仅可以保证用户良好观感外还能改善用户的操作体验,比如细长的屏幕就要将地图放的更靠下。方形的屏幕可以将道具区放到右边,释放牌区放到左边。
拥有各个区的大小以后,我们要将用户界面和游戏地图联系起来,游戏地图大小为(12*12), 而用户界面的大小却为 (420,420).所以我们需要创建一个方法来建立游戏界面与用户界面的联系
window.communicate =newCanvasCommunicatGame(
window.gameJson.mapSizeW,
window.gameJson.mapSizeH,
mapSize,
mapSize
);
CanvasCommunicatGame
对象我定义在文件 miegame.js 中 。
里面共有4个方法。
cw_to_gw
界面x转游戏x,
ch_to_gh
界面y转游戏y,
gw_to_cw
游戏x转界面x,
gh_to_ch
游戏y转界面y。
有4个方法以后就可以轻易的让用户界面与游戏核心建立联系,。
在设计时,设置的用户放地图的界面为正方形的,所以地图设计也应该为正方形的,这样子打印出来以后,地图才不会变型。
miegame_put_background();
函数会将除游戏以外的元素展现到用户的界面上。再把地图打印在应有的位置上。同时,箭头用户鼠标按下的动作,并且对应实现操作。
document.getElementById("botton_canvas").addEventListener("click",function(event){get_mouse_pos(document.getElementById("botton_canvas"), event);});
这个版本的程序并没有加入过多动画,如果需要加入动画的话,就应该实时刷新画布,而不是在用户有操作后再刷新画布。
五、⭐服务层的设计
实际上我这个服务层的设计是草草了事的,很多东西都没有做。这里主要讲解一下 PHP 与 MySQL 数据库的交互。
服务器获取Post表单的数据,
ask
为必须要有的数据 , 他表示现在如果没有 就默认为
ask = "main"
处理。
当
ask
为
main
时返回地区的星星数量。为
level
时为关卡地图请求。为newgame时为新游戏请求。
♾️5.1 PHP 获取请求者的IP地址 并且 获取他所在的地区
♾️5.1.1获取IP地址
如果你的服务器没有经过DNS服务,那么用最简单的方法就可以了。
$ip = $_SERVER['REMOTE_ADDR']
但是我的服务器经过了
CloudFlare
简称
CF
的DNS服务。那么久不可以通过上面的那个代码来获取用户的IP地址了,否者获取到的IP地址就是CF的DNS服务器的地址了。因为CF的IP报文可能不一定,Key有几种可能,所以我们为了使用方便,封装成了一个函数:
functionGET_IP(){global$ip;if($_SERVER['HTTP_CF_CONNECTING_IP'])$ip=$_SERVER['HTTP_CF_CONNECTING_IP'];elseif($_SERVER['REMOTE_ADDR'])$ip=$_SERVER['REMOTE_ADDR'];elseif($_SERVER['HTTP_X_FORWARDED_FOR'])$ip=$_SERVER['HTTP_X_FORWARDED_FOR'];elseif($_SERVER['HTTP_CLIENT_IP'])$ip=$_SERVER['HTTP_CLIENT_IP'];else$ip="Unknow";return$ip;}
这样子调用
GET_IP
方法就可以获取到用户的IP了。
♾️5.1.2通过IP获取用户所在地区
因为 IPV4 是动态分配的,所以不可以通过IP直接来确定用户的地理位置,只能通过IP数据库来获取。但是IP库又是一直更新的,维护起来特别麻烦。所以我们采用调用百度的API来实现;
functionget_city($star){$ip=GET_IP();//获取用户IPif(empty($ip)){return0;}$url='https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query='.$ip.'&co=&resource_id=6006&t=&ie=utf8&oe=gbk&cb=op_aladdin_callback&format=json&tn=baidu&cb=&_=';//调用了百度接口$str=file_get_contents($url);$encode=mb_detect_encoding($str,array("ASCII",'UTF-8',"GB2312","GBK",'BIG5'));$str=mb_convert_encoding($str,'UTF-8',$encode);//转化编码$str=json_decode($str);//转换为json类型$str=$str->data[0]->location;/*
//取出数据
foreach ($star as $value) {
if (!(stripos($str, $value["name"]) === FALSE)) {
return $value['id'];
}
}
*/return0;}
我们通过 baidu 的 API 获取IP的地理位置,并且与我们的数据库的市名称逐条比对,如果获取到的IP地理位置里存在我们数据库的市名称的话,那么他所在的市就是那一条。
♾️5.2 PHP 通过 Mysql 数据库获取信息
♾️5.2.1 PHP 连接数据库
我们要从MySQL数据库获取信息,第一步肯定是连接数据库了。
//从函数外引入数据库账号密码等global$servername,$sqlname,$sqlpassword,$sqldatabase;// 连接数据库$mysql=newmysqli($servername,$sqlname,$sqlpassword);// 转换字符集$mysql->set_charset('utf8');// 打开数据库if(!SQL\usebase($mysql,$sqldatabase)==true){// 如果打开失败则程序直接死亡$mysql->close();die();}
因为我连接数据库是写在方程里的,而数据库账号密码等均在函数外,所以第一步是从函数外引入数据库账号密码等。
global 【需要引入的变量】;
。引入后调用 mysqli 对象
mysqli(【服务器】, 【账号】, 【密码】)
连接。如果是本地服务器那么服务器就是
localhost
。把字符集设置为
UTF-8
,如果你是别的就设置成别的。最后一步是打开数据库,这里我使用的是我自己写的
MYSQL
库,过一段时间我完善后就会公开完整代码。本项目使用到的在项目里面都导入了。
♾️5.2.2 PHP获取数据库的表中的信息
下面的代码演示了 查询
level
表中的
address
条件是
id
要等于
要查的id
。
// 查询 level 表 中的 address 条件是 id 要等于 要查的id$yon=SQL\selectsql($mysql,"`level`","address","id",$id);if(!$yon==true){die();$mysql->close();}$map["address"]=mysqli_fetch_array($yon)[0];
当然我们也可以全部都查询比如在获取所有地区的星星数量的时候我们就使用了下面的代码。
查询 表
star
的
*
所有 信息 没有条件 所以
star
的后面要加上
--
。 不要漏了任何一个空格。这个写法很丑,实际上是因为我库没有完善。当然你叶可以直接通过 mysqli 的内置方法来查询。
// 查询 表 star 的 所有信息 没有条件$yon=SQL\selectsql($mysql,"`star` -- ","*","","");// 星星列表$star=[];// 把查询到的的信息全部都添加到 star 列表里面 while($row=$yon->fetch_assoc())//这里不能直接使用$row{array_push($star,$row);}
♾️5.2.3 PHP读取出地图文件并打乱牌
通过数据库获取到关卡文件存放的路径以后,就要读取出来并且打乱卡牌了。
// 通过路径读取出文件,并且将文件转换成字符串$mapJson=file_get_contents($map["address"]);// 调用打乱方法$mapJson=replay_map($mapJson);// 打乱方法的实现functionreplay_map($json,$random=true){// decode 解码 JSON$json=json_decode($json,true);// 扑克牌类型列表$pokerType=[];// 别管 if($random){// 给 pokerType 列表随机添加牌// 如果 i 小于 扑克总数则继续添加for($i=0;$i<$json["pokerAmount"];){// 生成随机的一种牌的类型,类型要在 扑克类型总数以内$type=mt_rand(1,$json['pokerTypeNum']);// 在pokerType列表后面添加3个这个牌的类型array_push($pokerType,$type,$type,$type);// i = i + 3$i+=3;}// 打乱 pokerType 列表shuffle($pokerType);// 给 地图 的扑克按照pokerType列表的顺序修改扑克类型for($i=0;$i<$json["pokerAmount"];$i++){$json["pokerList"][$i][2]=$pokerType[$i];}/* *************** 跳过阅读 ***************** */}else{$type=[];for($i=0;$i<$json['pokerTypeNum'];$i++){$type[$i]=$i+1;}shuffle($type);for($i=0;$i<$json["pokerAmount"];$i++){$json["pokerList"][$i][2]=$type[$json["pokerList"][$i][2]-1];}}/* *************** 继续阅读 ***************** */// 返回 编码后的 JSON 地图returnjson_encode($json);}
六、⭐JS 如何发送 HTTP 请求
♾️6.1 发送的方式
如果你要发送HTTP请求的话,你需要创建一个
XMLHttpRequest
(Chrome为内核的浏览器)对象
然后通过调用 open 方法来设置请求方法与请求列表。
可以使用
setRequestHeader
来设置请求头 , 例如 把
content-type
设置成
urlencoded
。
// 创建 XML HTTP 对象var xhr =newXMLHttpRequest;// 请求为 POST 请求 ,请求地址为 '/'
xhr.open("POST",'/',true);// 设置 Content-Type 为 application/x-www-form-urlencoded
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');// 设置回调函数 发送后的信息都由这个回调函数获得
xhr.onreadystatechange=function(){ console.log(xhr.readyState);};// 发送请求 ask = 1 , num = 2
xhr.send("ask=1&num=2");
这样子就可以发送 HTTP 请求了。
如果细心的人可以发现 控制台会输出4次数字。这代表 xhr.onreadystatechange 被调用了4次。
这 4 次 分别是
- 正在加载
- 请求已发送
- 交互中
- 加载完毕 所以一般我们会在
xhr.onreadystatechange
函数里面添加判断该语句 判断 当前的状态。
switch( xhr.readyState ){case0:
console.log("现在还没有被初始化");break;case1:
console.log("正在加载");break;case2:
console.log("请求已发送");break;case3:
console.log("交互中");break;case4:
console.log("加载完毕");break;}
♾️6.2 封装成函数
为了方便发送表单,我们把这个过程封装成函数
并且为了兼容其他浏览器,我们也要使用判断方法。
functionSERVER(url,postList){// POSTvar xhr;if(window.XMLHttpRequest){// Mozilla, Safari...
xhr =newXMLHttpRequest();}elseif(window.ActiveXObject){// IEtry{
xhr =newActiveXObject('Msxml2.XMLHTTP');}catch(e){try{
xhr =newActiveXObject('Microsoft.XMLHTTP');}catch(e){}}}if(xhr){
xhr.onreadystatechange = onReadyStateChange;
xhr.open('POST', url ,true);// 设置 Content-Type 为 application/x-www-form-urlencoded// 以表单的形式传递数据
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');let msg ="";for(let i in postList){
msg = msg + i +"="+ postList[i]+"&";}
xhr.send(msg);}// onreadystatechange 方法functiononReadyStateChange(){// 该函数会被调用四次
console.log(xhr.readyState);if(xhr.readyState ===4){// everything is good, the response is receivedif(xhr.status ===200){// 当请求的返回值是 200 成功 的话就打印回复的报文
console.log(xhr.responseText);}else{
console.log('There was a problem with the request.');}}else{// still not ready
console.log('still not ready...');}}}
利用这个方法我们就可以用简单的两句来实现 发送API 请求啦:
var url ="https://mrxie.xyz/"var form ={ask:"h"};var server =SERVER(url,form);
七、⭐地图文件格式解析
地图的文件格式是 Json ,里面一定得包括以下参数:
pokerAmount
扑克牌总数mapSizeW
地图宽mapSizeH
地图高pokerSize
扑克牌的边长pokerTypeNum
扑克类型总数pokerList
扑克列表
现版本仅支持正方形地图,也就是说,
mapSizeW
要等于
mapSizeH
。
{"pokerAmount":18,"mapSizeW":120,"mapSizeH":120,"pokerSize":10,"pokerTypeNum":1,"pokerList":[[10,10,1],[10,11,1],[10,12,1]]}
pokerList
pokerList
是存放每张扑克的数据的地方,按照 大号压小号 的规则编写。比如上面的例子中,
[10,12,1]
就压住了
[10,10,1]
。 这3个数据的意思分别是 扑克的
[x 坐标 ,y 坐标 ,t 种类]
。坐标均为
中心坐标
。
♾️7.1 地图文件的命名
普通的关卡地图均放在
./map/level/
文件夹下,以
level
加关卡数 加文件类型
.json
命名。例如
./map/level/level1.json
。
⭐预告
如果 CandyMieGame_PHP 、 CandyMieGame_H5 任意一项的 fork 超过 50 我就会发布 兼容 微信小程序的版本。
虽然没有达成目标 但是还是 : https://blog.csdn.net/apple_53792700/article/details/128041151
👋联系作者
✍️本文作者为 > 【谢玄.】 Mr-XieXuan < 于 2022/10/11/3:00 发布于 CSDN 。
📧E-mail:
[ Mr_Xie_@outlook.com ]
⌨️GitHub:
[ https://github.com/MR-XieXuan }
🔍个人私站:
[ https://main.mrxie.xyz/ ]
版权归原作者 谢玄. 所有, 如有侵权,请联系我们删除。