微信小游戲即將開放帘不?有我們在说莫,你還趕得上!(如何在完全不懂服務(wù)器開發(fā)的情況下做一個實(shí)時聯(lián)網(wǎng)對戰(zhàn)的微信小游戲)

根據(jù)微信官方對外公開的消息寞焙,微信小游戲的腳步越來越接近了储狭。它的開發(fā)者資格門檻和使用者門檻都很低互婿,以后必將引爆一波"全民開發(fā)小游戲"浪潮。

官方的開發(fā)工具創(chuàng)建項(xiàng)目即可獲取 打飛機(jī) 的源碼辽狈,這是一個很小但五臟俱全的2D游戲慈参,相信大多數(shù)嗅覺靈敏的程序員小哥哥們都已經(jīng)體驗(yàn)并且親手改造過啦。

但是如果你想借助微信的平臺刮萌,做一個交互性驮配、可玩性很強(qiáng)的 聯(lián)網(wǎng)游戲 ,就有一定的難度啦着茸。不用怕壮锻,有 Bmob 的最新產(chǎn)品 游戲SDK 助力,第一波流量紅利你也能輕松抓卒汤猜绣!這次教程我們就來討論 如何在完全不懂服務(wù)器開發(fā)的情況下做一個實(shí)時聯(lián)網(wǎng)對戰(zhàn)的微信小游戲 (聯(lián)網(wǎng)飛機(jī)大戰(zhàn))。

1.jpg


前言

為了能通讀這篇文章敬特,你最好:

  1. 已經(jīng)掌握開發(fā)簡單的微信小游戲掰邢,能看懂官方 打飛機(jī) 源碼就行,甚至?xí)?Javascript 輸出HelloWorld也行
  2. 略懂Java伟阔,其實(shí)不懂也行辣之,在JS的基礎(chǔ)上很容易引申,主要是要有 面向?qū)ο?/strong> 的思想

下文重點(diǎn)都是講如何快速上手開發(fā) 聯(lián)網(wǎng)的微信小游戲 减俏, 但 如果你懂得一些U3D開發(fā)召烂,Bmob官方 也同時提供了 Unity3D版本的Demo+SDK兩者可以跨平臺互通一起玩娃承,且接口規(guī)范高度一致,基本上覆蓋市面上所有的主流終端

PS:微信小游戲怕篷、Unity3D的SDK都是 開源 的,歡迎各位糾錯

2.png

最簡單的步驟

  1. 獲取 比目游戲云服務(wù) (下稱 官網(wǎng))的賬號历筝,文章下方有獲得方式;
  2. 官網(wǎng)下載 微信小游戲Demo+SDK廊谓,導(dǎo)入到微信開發(fā)者工具(下稱 工具)梳猪,并修改AppKey
  3. 官網(wǎng)配置玩家同步屬性蒸痹,并發(fā)布下載的云端代碼春弥,然后在官網(wǎng)選擇一個云服務(wù)器開啟(PS:云服務(wù)器是免費(fèi)的)
  4. 試運(yùn)行Demo叠荠,如果console沒有報(bào)錯的話匿沛,點(diǎn)擊工具預(yù)覽,用微信掃描二維碼榛鼎;
  5. 現(xiàn)在逃呼,就可以在游戲內(nèi)創(chuàng)建房間鳖孤,體驗(yàn)電腦與手機(jī)聯(lián)網(wǎng)對戰(zhàn)啦

接下來大概介紹一下微信小游戲項(xiàng)目開發(fā)的要點(diǎn)抡笼,云端代碼的詳解和U3D版本的教程將陸續(xù)推出

3.png

運(yùn)行效果

左邊的是 微信小游戲-開發(fā)者工具 的游戲頁面苏揣,與右邊的 Unity3D-MacOS-Editor 跨平臺玩

Demo測試運(yùn)行視頻 (B站無廣告?zhèn)魉烷T)

超清/720P模式觀看體驗(yàn)更好哦

不得不說程序員自己來做UI真的丑得可以,那個"房間"界面真的無力吐槽

目前的Demo跨平臺玩耍還有點(diǎn)小問題推姻,例如玩家平匈、怪物的移動速度不統(tǒng)一。但同平臺對戰(zhàn)是高度一致的藏古。 這個問題與SDK沒有關(guān)系增炭,都是Demo本地項(xiàng)目的參數(shù)設(shè)置,主要是因?yàn)閁nity項(xiàng)目都用的是絕對值校翔,微信小游戲項(xiàng)目都是相對值弟跑,后續(xù)Unity也采用相對值的方式,完善Demo防症。


如何從零開發(fā)

論游戲開發(fā)的經(jīng)驗(yàn)孟辑,相信各位讀者中比我厲害的人多了去了。我這里就根據(jù)我個人的開發(fā)歷程蔫敲,圍繞 聯(lián)網(wǎng)飛機(jī)大戰(zhàn) 這個項(xiàng)目饲嗽,講一下從零開發(fā)游戲的步驟吧。(嫌麻煩的可以不用看這一篇)

  1. 確定游戲主題奈嘿、玩法貌虾;
  2. 理清多個客戶端之間需要 同步的屬性、互相通知的事件裙犹;
  3. 分析客戶端與服務(wù)器需要 交互的事件尽狠;
  4. 制作/收集圖片、動畫叶圃、音效素材袄膏;
  5. 開發(fā)/照搬游戲世界的物理引擎,包括物體渲染掺冠、移動沉馆、碰撞檢測(以及內(nèi)存管理)等;
  6. 先開發(fā)服務(wù)端游戲邏輯(Java云端代碼)德崭,有利于理清整個游戲的邏輯斥黑;
  7. 后開發(fā)客戶端游戲邏輯、接入SDK眉厨;
  8. 測試锌奴、發(fā)布;
4.png

下面是展開來講 (獲取Demo缺猛、SDK完整源碼的方式見文章底部)


  • 玩法:這個項(xiàng)目準(zhǔn)備做成可以容納超多人同時在線的飛機(jī)大戰(zhàn)缨叫,所有設(shè)定基本上和微信小游戲官方Demo一樣椭符,增加了幾個設(shè)定:

      - 有四種造型、級別不同的Bot(有些人習(xí)慣稱為 '電腦'耻姥,也可以稱為'飛機(jī)NPC')
      - 第3销钝、4級的Bot可以開火,子彈(下稱Fire)飛行速度與玩家一致琐簇,4級Bot的開火頻率更高
      - Bot有生命值(不再是一碰就死)蒸健,分別是2、3婉商、4似忧、4,表示可以承受的Fire攻擊次數(shù)
      - Player(玩家)和Bot都分為兩個陣營丈秩,陣營內(nèi)無隊(duì)友傷害
      - Player的陣營由服務(wù)器隨機(jī)劃分盯捌,也可以改成玩家自己決定
      - 刷怪邏輯放在云端,指定新產(chǎn)生的Bot的陣營蘑秽、位置饺著、類型
      - Player受到傷害即淘汰,F(xiàn)ire碰到任何物體都消失
      - Player之間肠牲、Bot之間幼衰、Player與Bot 如果發(fā)生碰撞,會同歸于盡
      - Player的開火暫時做成自動的缀雳,而不是按鍵開火
      - Player的開火事件(開火坐標(biāo))是直接發(fā)送到其它客戶端渡嚣,不經(jīng)過云端代碼
      - Player的淘汰交由云端處理,由云端校驗(yàn)后肥印,再把該事件和勝負(fù)判定分發(fā)下去
      - Bot的淘汰判定交由云端處理识椰、分發(fā)
      - 當(dāng)某一方Player全部死亡時,另一方勝利深碱;雙方各剩一人時同歸于盡則平局
    

  • 客戶端間屬性同步裤唠、事件通知:玩家僅有兩個屬性需要自動同步、分發(fā)莹痢,一個是 位置,另一個是 分?jǐn)?shù)墓赴;直接同步的事件僅有 開火

      - 位置:這是一個2D游戲竞膳,所以玩家位置可以用float[2]類型表達(dá)
             但是為了保持一致性,Demo用了int[2]诫硕,數(shù)值由0-65535坦辟,表達(dá)0%-100%
             (一致性,是指跨平臺或分辨率章办、屏幕大小不同時锉走,坐標(biāo)需要達(dá)成一致最好用百分比)
      - 分?jǐn)?shù):僅云端代碼有權(quán)限修改滨彻,根據(jù)Player、Bot的擊落事件加分
             可以在游戲結(jié)束時挪蹭,結(jié)算成經(jīng)驗(yàn)值亭饵,保存到Bmob數(shù)據(jù)庫
             
      - 開火:直接通知到其它客戶端,僅記錄Fire的起點(diǎn)坐標(biāo)即可梁厉,也就是[0-65535,0-65535]
              表達(dá)成byte[]時辜羊,一個0-65535的int可以變成兩個0-255的數(shù)字組成
              再加上需要標(biāo)記這次通知的事件類型(開火),這里定flag為50
              也就是開火時向其它玩家發(fā)送 [50, 0-255, 0-255, 0-255, 0-255]
    
5.png

  • 客戶端-云端交互事件:需要服務(wù)器做的事情有:保存房間信息词顾;分配隊(duì)伍八秃;正式通知游戲開始;刷怪邏輯肉盹;判定Bot淘汰昔驱;判定Player淘汰;添加Player分?jǐn)?shù)上忍;判定勝負(fù)結(jié)果骤肛;戰(zhàn)績記錄

      - 房間、戰(zhàn)績信息:通過云端代碼的Bmob數(shù)據(jù)庫操作API完成
      - 分配隊(duì)伍:在客戶端Scene.OnLoad后通知服務(wù)器睡雇,服務(wù)器進(jìn)行隊(duì)伍分配
                將玩家隨機(jī)萌衬、均勻分成兩隊(duì),然后下發(fā)它抱,客戶端處理完畢再通知服務(wù)器
      - 正式開始:服務(wù)器確認(rèn)所有客戶端處理了隊(duì)伍信息后秕豫,通知所有客戶端開始游戲
      - 刷怪邏輯:隨機(jī)Bot的陣營、x軸位置观蓄、類型混移、名字,下發(fā)給客戶端處理
      - Bot淘汰:任意客戶端上報(bào)'目睹'某Bot被擊毀侮穿,云端即采信歌径、下發(fā)、記分
                所謂'目睹'亲茅,就是客戶端渲染時進(jìn)行碰撞檢測回铛,發(fā)現(xiàn)這個Bot的hp為0
      - Player淘汰:n個客戶端'目睹'某Player被擊毀,在短時間內(nèi)n>=m克锣,云端才采信茵肃、下發(fā)、記分
                   當(dāng)玩家僅有2袭祟、3人時验残,m為1,也就是上報(bào)即采信
                   當(dāng)玩家有4巾乳、5您没、6人時鸟召,m為2,不采信單個上報(bào)
                   當(dāng)玩家超過6人時氨鹏,m為3欧募,也就是起碼3人上報(bào)才采信
                   '短時間'目前是設(shè)為2000ms,也就是上報(bào)信息的有效期為2秒
      - 判定勝負(fù)結(jié)果:兩隊(duì)最后一人同時淘汰時平局喻犁;某隊(duì)先于敵隊(duì)全員淘汰則敗
    

  • 素材:來自美工/Unity Assets商店
6.png

  • 物理引擎:來自微信官方Demo(Sprite.js)/腦洞+造輪子/第三方途徑下載

      // 小改進(jìn)后的矩形碰撞檢測:
      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點(diǎn)已經(jīng)有說明槽片,這里放幾段代碼:
7.png

Room.java

    // public class Room extends RoomBase
    
    // 保存到Bmob數(shù)據(jù)庫的id
    public String mObjectId = null;
    // 先分配隊(duì)伍,后開始游戲肢础。分配隊(duì)伍這段時間还栓,不是真正的游戲開始,不要刷怪
    public boolean isNotReallyStart;
    // 刷怪的時間間隔(毫秒)传轰,決定了刷怪的頻率剩盒,根據(jù)玩家人數(shù)來定。人越多慨蛙,刷怪越快
    private long botSpawnSpan;
    // 上次刷怪的時間記錄
    private long lastBotSpawnTime = 0;
    // 怪物的個數(shù)辽聊,也順便作為id
    private long botCount = 0;
    // 置信區(qū)間: 計(jì)算擊中的邏輯放到了客戶端的時候,擊中敵人/怪物的事件期贫,不能完全聽信其中一個客戶端跟匆,防止ping差異擊殺、外掛
    // 怪物還相對無關(guān)緊要通砍,某一個客戶端上報(bào)了玛臂,就選擇相信他
    // 但是玩家的淘汰影響到體驗(yàn),需要多個玩家同時認(rèn)證的情況下判定
    // 于是約定:如果房間有2封孙、3人迹冤,可以一個人說了算(以免掉線玩家無敵)
    // 如果有4個人玩游戲,需要2個人在短時間內(nèi)"看到"某個玩家的死亡虎忌,那么這個玩家才是真正的死亡了
    // 更多人的情況下泡徙,最多只要3個人在短時間內(nèi)說某個玩家死亡,就可以作出判定
    // 特殊的膜蠢,如果某個玩家是匯報(bào)自己死亡堪藐,那么不用經(jīng)過置信區(qū)間檢測,直接判定死亡
    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);
        // 計(jì)算死亡判定的置信區(qū)間
        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:
                            // 房間關(guān)閉
                ));
        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;
        }
    }
    
    // 分配隊(duì)伍
    public void assignTeam() {
        // 游戲開始,所有玩家就位了寂嘉,將房間內(nèi)的玩家隨機(jī)奏瞬、平均分到兩隊(duì)
        // 服務(wù)器發(fā)送到客戶端的通知枫绅,就拿第一位當(dāng)作消息類型的區(qū)分吧(flag)
        for (Player p : players)
            p.teamId = 0;
        // 如果[1]=1,表示players[0]是隊(duì)伍1; [2]=0表示players[1]是隊(duì)伍2
        byte[] team = new byte[playerCount + 1];
        // (flag)1表示分隊(duì)情況
        team[0] = NotifyType_AssignTeam;
        // 其中一個隊(duì)的人數(shù)
        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來劃分并淋,越難打的怪出現(xiàn)幾率越低
        // 位置(主要是x軸)隨機(jī),按byte表示珍昨,0-255县耽,表示最左邊到最右邊,128是在屏幕中鍵

        // [0]表示flag镣典,這個通知是一個刷怪事件
        // [1]表示隊(duì)伍代號兔毙,這個怪是哪一邊的(和assignTeam的分配一致)
        // [2]表示刷怪點(diǎn)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) // 7、8
            botType = 2;
        else if (botType > 3) // 4兄春、5澎剥、6
            botType = 1;
        else
            botType = 0; // 0、1赶舆、2哑姚、3,默認(rèn)都是怪物0

        byte[] botName = ("Bot" + botType + "_" + Long.toHexString(botCount))
                .getBytes();
        byte[] botInfo = new byte[4 + botName.length];
        // (flag)2表示分隊(duì)情況
        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;

    // 不重復(fù)下發(fā)怪物死亡事件

    @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;
        // 檢查是否全部都準(zhǔn)備好了
        for (Player p : roommates)
            if (!p.isLoadOk)
                return;
        // 開始分配隊(duì)伍
        room.assignTeam();
    }

    @BmobGameSDKHook
    public strictfp void onAction_OnTeamInfoGet(byte[] bs) {
        // 收到了隊(duì)伍安排
        this.isTeamClear = true;
        // 檢查是否全部都準(zhǔn)備好了
        for (Player p : roommates)
            if (!p.isTeamClear)
                return;
        // 讓房間真正運(yùn)作起來
        room.reallyPlaying();
    }

    // 有玩家上報(bào)芜茵,發(fā)現(xiàn)某一個玩家死亡
    @BmobGameSDKHook
    public strictfp void onAction_PlayerCrash(byte[] infos) {
        if (room.isNotReallyStart || isDead)// 已經(jīng)死亡的玩家叙量,匯報(bào)不予采信
            return;
        // 注意,如果是敵機(jī)碰到自己九串,會發(fā)送兩條绞佩,一條說自己被對方撞死,另一條是對方被自己撞死蒸辆,這個時候都當(dāng)作是匯報(bào)自己死亡
        // 0: 墜機(jī)對象的no征炼,用byte表達(dá)的話,最多兼容256人大房間
        // 1: 傷害者類型(0: 敵方玩家(直接碰撞); 1: 敵方炮彈; 2: 敵方Bot)
        // 2: 如果是敵方玩家直接碰撞躬贡,那么對方的no是什么
        int dieNo = (int) infos[0];
        if (dieNo < 0 || dieNo > room.playerCount) {// 如果是128人以上的房間谆奥,dieNo可能是-127~-1,要考慮兼容
            kick(); // 不合法的上報(bào)拂玻,踢出玩家
            return;
        }
        int murdererNo = -1;
        if (infos[1] == 0) {
            murdererNo = (int) infos[2];
            if (murdererNo < 0 || murdererNo > room.playerCount) {
                kick(); // 不合法的上報(bào)酸些,踢出玩家
                return;
            }
        }
        if (dieNo == no || murdererNo == no) {
            // 給另外一個玩家添加一個死亡報(bào)告
            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) { // 有一個隊(duì)沒人了
            // 準(zhǔn)備發(fā)送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) { // 隊(duì)伍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(); // 游戲結(jié)束
        }
    }

    // 有玩家上報(bào)魄懂,怪物死亡
    @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))) {// 不重復(fù)的
            byte[] sendInfos = new byte[1 + infos.length];
            sendInfos[0] = Room.NotifyType_BotDie;
            arraycopy(infos, 0, sendInfos, 1, infos.length);
            sendToAll(sendInfos);
        }
    }

    // 游戲中掉線,當(dāng)作死亡
    @Override
    public void onOffline() {
        if (room.isNotReallyStart)
            return;
        die();
    }

    // 游戲中離開房間闯第,當(dāng)作死亡
    @Override
    public void onLeave() {
        if (room.isNotReallyStart)
            return;
        die();
    }
  • 接入SDK

      // game.js
          
      // 根據(jù)屏幕大小來定玩家的大小, 我們定玩家如果需要穿過整個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;
          }
      }
      
      // 其它玩家發(fā)送事件
      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;
          }
      }
    
  • 測試、發(fā)布:灰常好玩,下階段準(zhǔn)備做成四個陣營的玩法

開發(fā)體驗(yàn)

在基本素材填帽、組件(物理引擎)等預(yù)備充分的情況下蛛淋,花了不到兩個小時就將一個單機(jī)游戲改造成了聯(lián)網(wǎng)對戰(zhàn)的游戲,而且邏輯也足夠健壯篡腌,效果還是很酷的褐荷。再加上SDK是開源的,有什么問題很容易定位嘹悼。

總體來講叛甫,Bmob Game SDK真正拉低了網(wǎng)絡(luò)游戲開發(fā)的門檻,完全沒有了以前龐大杨伙、繁雜的后端開發(fā)和服務(wù)器運(yùn)維工作其监,讓很多受限于資源、只能開發(fā)單機(jī)游戲的團(tuán)隊(duì)和項(xiàng)目有了新的出路~

獲取Demo缀台、SDK完整源碼的方式:

加官方客服棠赛,小小琪QQ:2967459363
官方Q群:726133616


其他教程

落地成盒?Bmob幫你開發(fā)自己的聯(lián)網(wǎng)"吃雞"游戲

Unity聯(lián)網(wǎng)對戰(zhàn)游戲小Demo

如何實(shí)現(xiàn)各種游戲的思路雜想

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末膛腐,一起剝皮案震驚了整個濱河市睛约,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哲身,老刑警劉巖辩涝,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異勘天,居然都是意外死亡怔揩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門脯丝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來商膊,“玉大人,你說我怎么就攤上這事宠进≡尾穑” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵材蹬,是天一觀的道長实幕。 經(jīng)常有香客問我,道長堤器,這世上最難降的妖魔是什么昆庇? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮闸溃,結(jié)果婚禮上整吆,老公的妹妹穿的比我還像新娘拱撵。我一直安慰自己,他們只是感情好掂为,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布裕膀。 她就那樣靜靜地躺著,像睡著了一般勇哗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寸齐,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天欲诺,我揣著相機(jī)與錄音,去河邊找鬼渺鹦。 笑死扰法,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的毅厚。 我是一名探鬼主播塞颁,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吸耿!你這毒婦竟也來了祠锣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咽安,失蹤者是張志新(化名)和其女友劉穎伴网,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妆棒,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澡腾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年敌完,在試婚紗的時候發(fā)現(xiàn)自己被綠了返奉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡捷犹,死狀恐怖红选,靈堂內(nèi)的尸體忽然破棺而出澜公,到底是詐尸還是另有隱情,我是刑警寧澤纠脾,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布玛瘸,位于F島的核電站,受9級特大地震影響苟蹈,放射性物質(zhì)發(fā)生泄漏糊渊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一慧脱、第九天 我趴在偏房一處隱蔽的房頂上張望渺绒。 院中可真熱鬧,春花似錦、人聲如沸宗兼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽殷绍。三九已至染苛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間主到,已是汗流浹背茶行。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留登钥,地道東北人畔师。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像牧牢,于是被迫代替她去往敵國和親看锉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容