如何在完全不懂服务器开发的情况下做一个实时联网对战的微信小游戏
微信小游戏即将开放?有我们在,你还赶得上!
根据微信官方对外公开的消息,微信小游戏的脚步越来越接近了。它的开发者资格门槛和使用者门槛都很低,以后必将引爆一波”全民开发小游戏”浪潮。
官方的开发工具创建项目即可获取 打飞机
的源码,这是一个很小但五脏俱全的2D游戏,相信大多数嗅觉灵敏的程序员小哥哥们都已经体验并且亲手改造过啦。
但是如果你想借助微信的平台,做一个交互性、可玩性很强的 联网游戏
,就有一定的难度啦。不用怕,有 Bmob 的最新产品 游戏SDK 助力,第一波流量红利你也能轻松抓住!这次教程我们就来讨论 如何在完全不懂服务器开发的情况下做一个实时联网对战的微信小游戏 (联网飞机大战)。
前言
为了能通读这篇文章,你最好:
- 已经掌握开发简单的微信小游戏,能看懂官方
打飞机
源码就行,甚至会用Javascript
输出HelloWorld也行 - 略懂Java,其实不懂也行,在JS的基础上很容易引申,主要是要有 面向对象 的思想
下文重点都是讲如何快速上手开发 联网的微信小游戏 , 但 如果你懂得一些U3D开发,Bmob官方
也同时提供了 Unity3D版本的Demo+SDK
,两者可以跨平台互通一起玩,且接口规范高度一致,基本上覆盖市面上所有的主流终端
PS:微信小游戏、Unity3D的SDK都是 开源 的,欢迎各位纠错
最简单的步骤
- 获取 比目游戏云服务 (下称
官网
)的账号,文章下方有获得方式; - 在
官网
下载微信小游戏Demo+SDK
,导入到微信开发者工具
(下称工具
),并修改AppKey
; - 在
官网
配置玩家同步属性,并发布
下载的云端代码
,然后在官网
选择一个云服务器开启(PS:云服务器是免费的); - 试运行
Demo
,如果console
没有报错的话,点击工具
的预览
,用微信扫描二维码; - 现在,就可以在游戏内
创建房间
,体验电脑与手机联网对战啦;
接下来大概介绍一下微信小游戏项目开发的要点,云端代码的详解和U3D版本的教程将陆续推出
运行效果
左边的是 微信小游戏-开发者工具
的游戏页面,与右边的 Unity3D-MacOS-Editor
跨平台玩
Demo测试运行视频 (B站无广告传送门)
超清/720P模式观看体验更好哦
不得不说程序员自己来做UI真的丑得可以,那个”房间”界面真的无力吐槽
目前的Demo跨平台玩耍还有点小问题,例如玩家、怪物的移动速度不统一。但同平台对战是高度一致的。 这个问题与SDK没有关系,都是Demo本地项目的参数设置,主要是因为Unity项目都用的是绝对值,微信小游戏项目都是相对值,后续Unity也采用相对值的方式,完善Demo。
如何从零开发
论游戏开发的经验,相信各位读者中比我厉害的人多了去了。我这里就根据我个人的开发历程,围绕 联网飞机大战
这个项目,讲一下从零开发游戏的步骤吧。(嫌麻烦的可以不用看这一篇)
- 确定游戏主题、玩法;
- 理清多个客户端之间需要 同步的属性、互相通知的事件;
- 分析客户端与服务器需要 交互的事件;
- 制作/收集图片、动画、音效素材;
- 开发/照搬游戏世界的物理引擎,包括物体渲染、移动、碰撞检测(以及内存管理)等;
- 先开发服务端游戏逻辑(Java云端代码),有利于理清整个游戏的逻辑;
- 后开发客户端游戏逻辑、接入SDK;
- 测试、发布;
下面是展开来讲 (获取Demo、SDK完整源码的方式见文章底部)
玩法:这个项目准备做成可以容纳超多人同时在线的飞机大战,所有设定基本上和微信小游戏官方Demo一样,增加了几个设定:
- 有四种造型、级别不同的Bot(有些人习惯称为 '电脑',也可以称为'飞机NPC') - 第3、4级的Bot可以开火,子弹(下称Fire)飞行速度与玩家一致,4级Bot的开火频率更高 - Bot有生命值(不再是一碰就死),分别是2、3、4、4,表示可以承受的Fire攻击次数 - Player(玩家)和Bot都分为两个阵营,阵营内无队友伤害 - Player的阵营由服务器随机划分,也可以改成玩家自己决定 - 刷怪逻辑放在云端,指定新产生的Bot的阵营、位置、类型 - Player受到伤害即淘汰,Fire碰到任何物体都消失 - Player之间、Bot之间、Player与Bot 如果发生碰撞,会同归于尽 - Player的开火暂时做成自动的,而不是按键开火 - Player的开火事件(开火坐标)是直接发送到其它客户端,不经过云端代码 - Player的淘汰交由云端处理,由云端校验后,再把该事件和胜负判定分发下去 - Bot的淘汰判定交由云端处理、分发 - 当某一方Player全部死亡时,另一方胜利;双方各剩一人时同归于尽则平局
客户端间属性同步、事件通知:玩家仅有两个属性需要自动同步、分发,一个是
位置
,另一个是分数
;直接同步的事件仅有开火
- 位置:这是一个2D游戏,所以玩家位置可以用float[2]类型表达但是为了保持一致性,Demo用了int[2],数值由0-65535,表达0%-100%(一致性,是指跨平台或分辨率、屏幕大小不同时,坐标需要达成一致最好用百分比) - 分数:仅云端代码有权限修改,根据Player、Bot的击落事件加分可以在游戏结束时,结算成经验值,保存到Bmob数据库- 开火:直接通知到其它客户端,仅记录Fire的起点坐标即可,也就是[0-65535,0-65535]表达成byte[]时,一个0-65535的int可以变成两个0-255的数字组成再加上需要标记这次通知的事件类型(开火),这里定flag为50也就是开火时向其它玩家发送 [50, 0-255, 0-255, 0-255, 0-255]
客户端-云端交互事件:需要服务器做的事情有:保存房间信息;分配队伍;正式通知游戏开始;刷怪逻辑;判定Bot淘汰;判定Player淘汰;添加Player分数;判定胜负结果;战绩记录
- 房间、战绩信息:通过云端代码的Bmob数据库操作API完成 - 分配队伍:在客户端Scene.OnLoad后通知服务器,服务器进行队伍分配将玩家随机、均匀分成两队,然后下发,客户端处理完毕再通知服务器 - 正式开始:服务器确认所有客户端处理了队伍信息后,通知所有客户端开始游戏 - 刷怪逻辑:随机Bot的阵营、x轴位置、类型、名字,下发给客户端处理 - Bot淘汰:任意客户端上报'目睹'某Bot被击毁,云端即采信、下发、记分所谓'目睹',就是客户端渲染时进行碰撞检测,发现这个Bot的hp为0 - Player淘汰:n个客户端'目睹'某Player被击毁,在短时间内n>=m,云端才采信、下发、记分当玩家仅有2、3人时,m为1,也就是上报即采信当玩家有4、5、6人时,m为2,不采信单个上报当玩家超过6人时,m为3,也就是起码3人上报才采信'短时间'目前是设为2000ms,也就是上报信息的有效期为2秒 - 判定胜负结果:两队最后一人同时淘汰时平局;某队先于敌队全员淘汰则败
- 素材:来自美工/Unity Assets商店
物理引擎:来自微信官方Demo(Sprite.js)/脑洞+造轮子/第三方途径下载
// 小改进后的矩形碰撞检测: isCollideWith(sp) {if (this.visible && sp.visible) {let dis = sp.x - this.x;if (-sp.width < dis && dis < this.width) {dis = sp.y - this.y;if (-sp.height < dis && dis < this.height)return true;}}return false; }
- Java云端代码:在上面第3点已经有说明,这里放几段代码:
Room.java:
// public class Room extends RoomBase// 保存到Bmob数据库的idpublic String mObjectId = null;// 先分配队伍,后开始游戏。分配队伍这段时间,不是真正的游戏开始,不要刷怪public boolean isNotReallyStart;// 刷怪的时间间隔(毫秒),决定了刷怪的频率,根据玩家人数来定。人越多,刷怪越快private long botSpawnSpan;// 上次刷怪的时间记录private long lastBotSpawnTime = 0;// 怪物的个数,也顺便作为idprivate long botCount = 0;// 置信区间: 计算击中的逻辑放到了客户端的时候,击中敌人/怪物的事件,不能完全听信其中一个客户端,防止ping差异击杀、外挂// 怪物还相对无关紧要,某一个客户端上报了,就选择相信他// 但是玩家的淘汰影响到体验,需要多个玩家同时认证的情况下判定// 于是约定:如果房间有2、3人,可以一个人说了算(以免掉线玩家无敌)// 如果有4个人玩游戏,需要2个人在短时间内"看到"某个玩家的死亡,那么这个玩家才是真正的死亡了// 更多人的情况下,最多只要3个人在短时间内说某个玩家死亡,就可以作出判定// 特殊的,如果某个玩家是汇报自己死亡,那么不用经过置信区间检测,直接判定死亡public int confidenceInterval = 1;private final Set<String> dieBotsNames = new HashSet<String>();public static final byte//NotifyType_AssignTeam = 1,//NotifyType_BotSpawn = 2,//NotifyType_ReallyStart = 3,//NotifyType_PlayerCrash = 4,//NotifyType_BotDie = 5,//NotifyType_GameOver = 6//;@Overridepublic void onCreate() {// 各1个玩家的时候,1秒2个怪;以此类推// botSpawnSpan = (1000 / 2) / (playerCount / 2);botSpawnSpan = (2000) / (playerCount / 2);// 计算死亡判定的置信区间if (playerCount > 3)confidenceInterval = 2;else if (playerCount > 5)confidenceInterval = 3;HttpResponse response = Bmob.getInstance().insert("Room", JSON.toJson(//"roomId", roomId,//"master", masterId,//"masterKey", masterKey,//"joinKey", joinKey,//"playerCount", playerCount,//"address", address,//"tcpPort", tcpPort,//"udpPort", udpPort,//"websocketPort", websocketPort,//"status", 0// 0: 开启中,1: 游戏中,2:// 房间关闭));mObjectId = response.jsonData.getString("objectId");}@Overridepublic void onGameStart() {if (!Functions.isStrEmpty(mObjectId))Bmob.getInstance().update("Room", mObjectId,JSON.toJson("status", 1));dieBotsNames.clear();isNotReallyStart = true;lastBotSpawnTime = 0;botCount = 0;}@Overridepublic void onDestroy() {if (!Functions.isStrEmpty(mObjectId))Bmob.getInstance().update("Room", mObjectId,JSON.toJson("status", 2));}@Override@BmobGameSDKHookpublic void onTick() {if (isNotReallyStart)return;long curTime = getTime();if (curTime > lastBotSpawnTime + botSpawnSpan) {spawnBot();lastBotSpawnTime = curTime;}}// 分配队伍public void assignTeam() {// 游戏开始,所有玩家就位了,将房间内的玩家随机、平均分到两队// 服务器发送到客户端的通知,就拿第一位当作消息类型的区分吧(flag)for (Player p : players)p.teamId = 0;// 如果[1]=1,表示players[0]是队伍1; [2]=0表示players[1]是队伍2byte[] team = new byte[playerCount + 1];// (flag)1表示分队情况team[0] = NotifyType_AssignTeam;// 其中一个队的人数int team1Count = playerCount / 2;while (team1Count != 0) {int id = ((int) (Math.random() * 100000) % playerCount) + 1;if (team[id] != 1) {players[id - 1].teamId = 1;team[id] = 1;team1Count--;}}sendToAll(team);}// 刷怪private void spawnBot() {botCount++;// 游戏里面有4种难度不同的怪,将概率按1:2:3:4来划分,越难打的怪出现几率越低// 位置(主要是x轴)随机,按byte表示,0-255,表示最左边到最右边,128是在屏幕中键// [0]表示flag,这个通知是一个刷怪事件// [1]表示队伍代号,这个怪是哪一边的(和assignTeam的分配一致)// [2]表示刷怪点x轴的位置// [3]表示怪物种类// [4-]表示怪物名(Bot[Type]_[Id])byte botTeam = (byte) (((int) (Math.random() * 100)) % 2);byte botPositionX = (byte) (((int) (Math.random() * 0xffff)) & 0xff);byte botType = (byte) (Math.random() * 10); // 0-9if (botType == 9) // 9botType = 3;else if (botType > 6) // 7、8botType = 2;else if (botType > 3) // 4、5、6botType = 1;elsebotType = 0; // 0、1、2、3,默认都是怪物0byte[] botName = ("Bot" + botType + "_" + Long.toHexString(botCount)).getBytes();byte[] botInfo = new byte[4 + botName.length];// (flag)2表示分队情况botInfo[0] = NotifyType_BotSpawn;botInfo[1] = botTeam;botInfo[2] = botPositionX;botInfo[3] = botType;arraycopy(botName, 0, botInfo, 4, botName.length);sendToAll(botInfo);}
–
Player.java:
// public class Player extends PlayerBasepublic int teamId = 0;private boolean isDead = false;private boolean isLoadOk = false, isTeamClear = false;private long[] dieReports;// 不重复下发怪物死亡事件@BmobGameSDKHookpublic native void setIsDead(boolean isDead);@Overridepublic void onGameStart() {dieReports = new long[room.playerCount];isLoadOk = false;isDead = false;setIsDead(isDead);syncToClient();}@BmobGameSDKHookpublic strictfp void onAction_OnGameLoad(byte[] bs) {// 加载好了游戏场景this.isLoadOk = true;// 检查是否全部都准备好了for (Player p : roommates)if (!p.isLoadOk)return;// 开始分配队伍room.assignTeam();}@BmobGameSDKHookpublic strictfp void onAction_OnTeamInfoGet(byte[] bs) {// 收到了队伍安排this.isTeamClear = true;// 检查是否全部都准备好了for (Player p : roommates)if (!p.isTeamClear)return;// 让房间真正运作起来room.reallyPlaying();}// 有玩家上报,发现某一个玩家死亡@BmobGameSDKHookpublic strictfp void onAction_PlayerCrash(byte[] infos) {if (room.isNotReallyStart || isDead)// 已经死亡的玩家,汇报不予采信return;// 注意,如果是敌机碰到自己,会发送两条,一条说自己被对方撞死,另一条是对方被自己撞死,这个时候都当作是汇报自己死亡// 0: 坠机对象的no,用byte表达的话,最多兼容256人大房间// 1: 伤害者类型(0: 敌方玩家(直接碰撞); 1: 敌方炮弹; 2: 敌方Bot)// 2: 如果是敌方玩家直接碰撞,那么对方的no是什么int dieNo = (int) infos[0];if (dieNo < 0 || dieNo > room.playerCount) {// 如果是128人以上的房间,dieNo可能是-127~-1,要考虑兼容kick(); // 不合法的上报,踢出玩家return;}int murdererNo = -1;if (infos[1] == 0) {murdererNo = (int) infos[2];if (murdererNo < 0 || murdererNo > room.playerCount) {kick(); // 不合法的上报,踢出玩家return;}}if (dieNo == no || murdererNo == no) {// 给另外一个玩家添加一个死亡报告if (dieNo == no) {if (murdererNo != -1)roommates[murdererNo].reportDie(this);} elseroommates[dieNo].reportDie(this);die();// 本玩家死亡} else { // 观察其它玩家的死亡roommates[dieNo].reportDie(this);}}void reportDie(Player reporter) {if (room.isNotReallyStart || isDead) // 死猪不怕开水烫return;long curTime = getTime();dieReports[reporter.no] = curTime;int dieCount = 0;long reportExpired = curTime - 2000;for (long time : dieReports)if (time > reportExpired)dieCount++;if (dieCount < room.confidenceInterval)return;die();}void die() {isDead = true;setIsDead(isDead);syncToClient();sendToAll(new byte[] { Room.NotifyType_PlayerCrash, (byte) no });int[] teamAliveCounts = new int[] { 0, 0 };String msg = String.format("Player[%d][%s] die\n", no, getUserId());for (Player p : roommates) {if (p.isDead) {msg += p.no + " is dead, team " + p.teamId + "\n";continue;}teamAliveCounts[p.teamId]++;msg += p.no + " is alive, team " + p.teamId + "\n";}msg += String.format("team_0 has alive[%d] and team_1 is [%d]", no,teamAliveCounts[0], teamAliveCounts[1]);if (teamAliveCounts[0] == 0 || teamAliveCounts[1] == 0) { // 有一个队没人了// 准备发送GameOver, 0:平局,1:胜利,2:失败byte[] toTeam0 = new byte[] { Room.NotifyType_GameOver, 0 }, //toTeam1 = new byte[] { Room.NotifyType_GameOver, 0 };if (teamAliveCounts[0] == teamAliveCounts[1]) {// 都没人了} else if (teamAliveCounts[0] == 0) { // 队伍1胜利toTeam0[1] = 2;toTeam1[1] = 1;} else {toTeam0[1] = 1;toTeam1[1] = 2;}for (Player p : roommates)p.send(p.teamId == 0 ? toTeam0 : toTeam1);room.gameOver(); // 游戏结束}}// 有玩家上报,怪物死亡@BmobGameSDKHookpublic strictfp void onAction_BotDie(byte[] infos) { // 暂时放怪物名if (room.isNotReallyStart)return;// cn.bmob.gamesdk.server.Main.l("BotDie: (" +// java.util.Arrays.toString(infos) + ") : " + infos.length);if (room.isBotDieNow(new String(infos))) {// 不重复的byte[] sendInfos = new byte[1 + infos.length];sendInfos[0] = Room.NotifyType_BotDie;arraycopy(infos, 0, sendInfos, 1, infos.length);sendToAll(sendInfos);}}// 游戏中掉线,当作死亡@Overridepublic void onOffline() {if (room.isNotReallyStart)return;die();}// 游戏中离开房间,当作死亡@Overridepublic void onLeave() {if (room.isNotReallyStart)return;die();}
接入SDK:
// game.js// 根据屏幕大小来定玩家的大小, 我们定玩家如果需要穿过整个y轴最少需要2秒,怪物需要8秒 const PlayerMaxSpeed = screenHeight / 2000; // px per sec const BotSpeed = screenHeight / 8000; // px per sec const EnemyFireSpeed = screenHeight / 3000; // px per sec const FriendFireSpeed = -EnemyFireSpeed;// 其它玩家更新属性 onOthersStatus(no, changedAttr, hisStatus) {if (changedAttr.position) {let y = hisStatus.position[1];let gameObj = this.players[no].gameObject;if (gameObj.isTeammate)y = 65535 - y;gameObj.x = hisStatus.position[0] / WidthRatio - PlayerWidth / 2;gameObj.y = y / HeightRatio - PlayerHeight / 2;} }// 其它玩家发送事件 onTransfer(no, body) {switch (body.shift()) {case 50:console.log('Fire from: ', this.players[no]);let isTeammate = this.players[no].gameObject.isTeammate,x = (body[0] << 8) | body[1],y = (body[2] << 8) | body[3];if (isTeammate)y = 65535 - y;let fire = new Sprite(isTeammate ? ImgSrc_Fire_Friend : ImgSrc_Fire_Enemy,FireWidth,FireHeight,x / WidthRatio,y / HeightRatio);fire.objType = 3; // 0: sundries; 1: player; 2: bot; 3: firefire.velocity = isTeammate ? FriendFireSpeed : EnemyFireSpeed;fire.teamId = isTeammate ? this.mTeamId : (1 - this.mTeamId);this.gameObjArr.push(fire);break;} }// 云端通知 onCloudNotify(notify) {switch (notify.shift()) {case NotifyType_AssignTeam:this.assignTeam(notify);break;case NotifyType_BotSpawn:this.botSpawn(notify[0] == this.mTeamId,(notify[1]) * screenWidth / 255,notify[2],model.bytesToString(notify, 3, notify.length));break;case NotifyType_ReallyStart:this.startGame();break;case NotifyType_PlayerCrash:this.renderPlayerDie(notify[0]);break;case NotifyType_BotDie:this.botDie(model.bytesToString(notify, 0, notify.length));break;case NotifyType_GameOver:this.isGameStart = false;switch (notify[0]) {case 0:this.gameDraw();break;case 1:this.gameWin();break;case 2:this.gameLose();break;}break;} }
测试、发布:灰常好玩,下阶段准备做成四个阵营的玩法
开发体验
在基本素材、组件(物理引擎)等预备充分的情况下,花了不到两个小时就将一个单机游戏改造成了联网对战的游戏,而且逻辑也足够健壮,效果还是很酷的。再加上SDK是开源的,有什么问题很容易定位。
总体来讲,Bmob Game SDK真正拉低了网络游戏开发的门槛,完全没有了以前庞大、繁杂的后端开发和服务器运维工作,让很多受限于资源、只能开发单机游戏的团队和项目有了新的出路~
获取Demo、SDK完整源码的方式:
加官方客服,小小琪QQ:2967459363
其他教程
落地成盒?Bmob帮你开发自己的联网”吃鸡”游戏
Unity联网对战游戏小Demo
如何实现各种游戏的思路杂想
如何在完全不懂服务器开发的情况下做一个实时联网对战的微信小游戏相关推荐
- 想开发微信小游戏,先看看腾讯是如何制定规则的
点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 作者 | 凌华彬.王哲 责编 | 徐威龙 一.前言 在第一篇文章<微信小游戏开发上手& ...
- 中级 | 想开发微信小游戏,先看看腾讯是如何制定规则的!
作者 | 凌华彬.王哲 一.前言 在第一篇文章<微信小游戏开发上手>中,我们给大家介绍了上手微信小游戏开发所需要的知识,以及小游戏的开发环境和工具.学会如何开发小游戏固然重要,但是更重要的 ...
- 微直播笔记|三十分钟微信小游戏开发入门
微信小游戏在2017年12月28日正式上线.相对于传统的H5游戏,小游戏的优势十分明显,拥有微信庞大的用户量以及更好的兼容性,在天生适合微信社交生态的同时还不用担心被屏蔽.无疑,这或许是一个巨大的风口 ...
- 新手入门:如何用Laya开发微信小游戏?
1.环境准备 1.1 LayaAirIDE 1.7.14版本才开始集成微信小游戏开发. 1.2 微信小游戏开发工具 微信小游戏开发工具是小游戏开发与测试的环境,由于LayaAir引擎的开发者完全可以使 ...
- 【Linux服务器开发系列】手写一个用户态网络协议栈,瞬间提升你网络功底丨netmap/dpdk的实现
手写一个用户态网络协议栈,瞬间提升你网络功底 1. 网卡基础架构 2. netmap/dpdk的实现 3. 网络协议栈实战 [Linux服务器开发系列]手写一个用户态网络协议栈,瞬间提升你网络功底丨n ...
- 利用游戏服务器引擎Matchvs开发联网微信小游戏
Matchvs是一款十分易用的游戏服务器引擎,开发者通过SaaS层提供的API,即可完成"就近"节点接入.玩家匹配.游戏数据通信的建立:PaaS提供GS开发框架.托管平台,无需自有 ...
- 张小龙「跳一跳」都玩到 6000 多分,是时候掌握微信小游戏高级开发了
点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 作者 | 凌华彬.王哲 责编 | 徐威龙 [CSDN 编者按]今天,在 2018 年度微信公 ...
- 微信小游戏云开发 | 72小时极限编程体验
七夕之夜,Shawn 的 "消消大冒险❤七夕特别版" 算是蹭上一个热点,经过这两天的实践,答出了一个结论:个人开发者靠朋友圈.公众号.微信亲友群.微信技术群.微信小游戏互点群.QQ ...
- 搭建微信小游戏开发环境总结
这篇文章主要解决以下问题 1.一键申请泛域名证书并到期自动更新 2.Nginx配置https 3.本地资源映射到外网 4.介绍CocosCreator构建发布微信小游戏时远程服务器地址如何配置 文章目 ...
最新文章
- ※交换排序(1)——快速排序(quick sort)
- php在window,php在window上的问题
- 让皮肤美白细致的七大DIY - 生活至上,美容至尚!
- python importlib qpython_Python imports指南:Python的导入有更好的理解
- poj Labeling Balls 3687 拓扑排序!!!!
- Java库转oc,(java转行oc)什么是block,用java来解释oc中的block
- python表白代码大全-python表白代码
- 史海峰:我的架构师修炼之道
- word总页数不包含封面_word2007插入页码怎么让总页数不算封面
- 发射光功率和接收灵敏度对光模块的实际使用有什么影响?
- 互联网思维之社会化思维
- 做直流逆变中用到的全桥逆变电路测试mos管好坏的方法
- ACM-ICPC 2018 沈阳赛区网络预赛 F. Fantastic Graph (有上下界可行流)
- solr 查询出异常org.apache.solr.search.SyntaxError: Cannot parse
- C语言标准ANSI C、C语言的特点、C语言的关键字(32个)
- 虚拟机Linux忘记root用户密码的修复方法
- 商城-商品规格管理-商品规格参数管理
- InnoDB存储引擎:引擎概况
- 如何将白鹭引擎开发的游戏通过Egret Native发布到 GooglePlay平台
- anacnda 子环境管理