微信小遊戲即將開放?有咱們在,你還趕得上!java
根據微信官方對外公開的消息,微信小遊戲的腳步愈來愈接近了。它的開發者資格門檻和使用者門檻都很低,之後必將引爆一波"全民開發小遊戲"浪潮。程序員
官方的開發工具建立項目便可獲取 打飛機
的源碼,這是一個很小但五臟俱全的2D遊戲,相信大多數嗅覺靈敏的程序員小哥哥們都已經體驗而且親手改造過啦。web
可是若是你想借助微信的平臺,作一個交互性、可玩性很強的 聯網遊戲
,就有必定的難度啦。不用怕,有 Bmob 的最新產品 遊戲SDK 助力,第一波流量紅利你也能輕鬆抓住!此次教程咱們就來討論 如何在徹底不懂服務器開發的狀況下作一個實時聯網對戰的微信小遊戲 (聯網飛機大戰)。數據庫
爲了能通讀這篇文章,你最好:json
打飛機
源碼就行,甚至會用 Javascript
輸出HelloWorld也行下文重點都是講如何快速上手開發 聯網的微信小遊戲 , 但 若是你懂得一些U3D開發,Bmob官方
也同時提供了 Unity3D版本的Demo+SDK
,二者能夠跨平臺互通一塊兒玩,且接口規範高度一致,基本上覆蓋市面上全部的主流終端 segmentfault
PS:微信小遊戲、Unity3D的SDK都是 開源 的,歡迎各位糾錯後端
官網
)的帳號,文章下方有得到方式;官網
下載 微信小遊戲Demo+SDK
,導入到微信開發者工具
(下稱 工具
),並修改AppKey
;官網
配置玩家同步屬性,並發佈
下載的雲端代碼
,而後在官網
選擇一個雲服務器開啓(PS:雲服務器是免費的);Demo
,若是console
沒有報錯的話,點擊工具
的預覽
,用微信掃描二維碼;建立房間
,體驗電腦與手機聯網對戰啦;接下來大概介紹一下微信小遊戲項目開發的要點,雲端代碼的詳解和U3D版本的教程將陸續推出服務器
左邊的是 微信小遊戲-開發者工具
的遊戲頁面,與右邊的 Unity3D-MacOS-Editor
跨平臺玩微信
Demo測試運行視頻 (B站無廣告傳送門) websocket
超清/720P模式觀看體驗更好哦
不得不說程序員本身來作UI真的醜得能夠,那個"房間"界面真的無力吐槽
目前的Demo跨平臺玩耍還有點小問題,例如玩家、怪物的移動速度不統一。但同平臺對戰是高度一致的。 這個問題與SDK沒有關係,都是Demo本地項目的參數設置,主要是由於Unity項目都用的是絕對值,微信小遊戲項目都是相對值,後續Unity也採用相對值的方式,完善Demo。
論遊戲開發的經驗,相信各位讀者中比我厲害的人多了去了。我這裏就根據我我的的開發歷程,圍繞 聯網飛機大戰
這個項目,講一下從零開發遊戲的步驟吧。(嫌麻煩的能夠不用看這一篇)
下面是展開來說 (獲取Demo、SDK完整源碼的方式見文章底部)
玩法:這個項目準備作成能夠容納超多人同時在線的飛機大戰,全部設定基本上和微信小遊戲官方Demo同樣,增長了幾個設定:
客戶端間屬性同步、事件通知:玩家僅有兩個屬性須要自動同步、分發,一個是 位置
,另外一個是 分數
;直接同步的事件僅有 開火
客戶端-雲端交互事件:須要服務器作的事情有:保存房間信息;分配隊伍;正式通知遊戲開始;刷怪邏輯;斷定Bot淘汰;斷定Player淘汰;添加Player分數;斷定勝負結果;戰績記錄
物理引擎:來自微信官方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;
}
Room.java:
// public class Room extends RoomBase // 保存到Bmob數據庫的id public String mObjectId = null; // 先分配隊伍,後開始遊戲。分配隊伍這段時間,不是真正的遊戲開始,不要刷怪 public boolean isNotReallyStart; // 刷怪的時間間隔(毫秒),決定了刷怪的頻率,根據玩家人數來定。人越多,刷怪越快 private long botSpawnSpan; // 上次刷怪的時間記錄 private long lastBotSpawnTime = 0; // 怪物的個數,也順便做爲id private long botCount = 0; // 置信區間: 計算擊中的邏輯放到了客戶端的時候,擊中敵人/怪物的事件,不能徹底聽信其中一個客戶端,防止ping差別擊殺、外掛 // 怪物還相對可有可無,某一個客戶端上報了,就選擇相信他 // 可是玩家的淘汰影響到體驗,須要多個玩家同時認證的狀況下斷定 // 因而約定:若是房間有二、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// ; @Override public 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"); } @Override public void onGameStart() { if (!Functions.isStrEmpty(mObjectId)) Bmob.getInstance().update("Room", mObjectId, JSON.toJson("status", 1)); dieBotsNames.clear(); isNotReallyStart = true; lastBotSpawnTime = 0; botCount = 0; } @Override public void onDestroy() { if (!Functions.isStrEmpty(mObjectId)) Bmob.getInstance().update("Room", mObjectId, JSON.toJson("status", 2)); } @Override @BmobGameSDKHook public 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]是隊伍2 byte[] 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-9 if (botType == 9) // 9 botType = 3; else if (botType > 6) // 七、8 botType = 2; else if (botType > 3) // 四、五、6 botType = 1; else botType = 0; // 0、一、二、3,默認都是怪物0 byte[] 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 PlayerBase public int teamId = 0; private boolean isDead = false; private boolean isLoadOk = false, isTeamClear = false; private long[] dieReports; // 不重複下發怪物死亡事件 @BmobGameSDKHook public native void setIsDead(boolean isDead); @Override public void onGameStart() { dieReports = new long[room.playerCount]; isLoadOk = false; isDead = false; setIsDead(isDead); syncToClient(); } @BmobGameSDKHook public strictfp void onAction_OnGameLoad(byte[] bs) { // 加載好了遊戲場景 this.isLoadOk = true; // 檢查是否所有都準備好了 for (Player p : roommates) if (!p.isLoadOk) return; // 開始分配隊伍 room.assignTeam(); } @BmobGameSDKHook public strictfp void onAction_OnTeamInfoGet(byte[] bs) { // 收到了隊伍安排 this.isTeamClear = true; // 檢查是否所有都準備好了 for (Player p : roommates) if (!p.isTeamClear) return; // 讓房間真正運做起來 room.reallyPlaying(); } // 有玩家上報,發現某一個玩家死亡 @BmobGameSDKHook public 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); } else roommates[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(); // 遊戲結束 } } // 有玩家上報,怪物死亡 @BmobGameSDKHook public 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); } } // 遊戲中掉線,看成死亡 @Override public void onOffline() { if (room.isNotReallyStart) return; die(); } // 遊戲中離開房間,看成死亡 @Override public 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: fire fire.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真正拉低了網絡遊戲開發的門檻,徹底沒有了之前龐大、繁雜的後端開發和服務器運維工做,讓不少受限於資源、只能開發單機遊戲的團隊和項目有了新的出路~
加官方客服,小小琪QQ:2967459363