用Unity做個遊戲(十) - 完結篇,內容補全

Inspoy Cheng發表於2017-09-23

本文首發自inspoy的雜七雜八 | 菜雞inspoy的學習記錄

前言

這個專案差不多5月份就已經沒有再更新了,6月初正式從公司離職開始專心做獨立遊戲了。差不多到現在已經一個月了,工作也慢慢進入了正軌,這兩天手頭暫時閒下來了,也差不多該把這個系列完結掉了,了卻我一樁心願233

服務端主要遊戲邏輯

上次說到主要邏輯是由各個具體的Controller來實現的,這個遊戲分為兩個Controller:UserControllerBattleController
前者主要負責使用者的登陸登出等等,邏輯比較簡單,我們主要來看BattleController的邏輯
這個Controller只處理一個協議,就是SFRequestMsgUnitSync同步狀態協議,裡面包含4個引數,移動方向,滑鼠朝向和是否釋放了技能。
處理協議的邏輯如下:

/**
 * 使用者同步操作資訊
 * @param req
 */
const onUserSyncInfo = function (req) {
    let retCode = 0;
    do {
        const battleId = userData.onlineUserList[req.uid].battleId;
        const battle = battleData.battleList[battleId];
        const userItem = battle.users[req.uid];
        userItem.accX = req["moveX"] * userItem.accPower / userItem.mass;
        userItem.accY = req["moveY"] * userItem.accPower / userItem.mass;
        userItem.rotation = req["rotation"];
        userItem.skillId = req["skillId"];
    } while (false);
    const resp = {pid: 3, retCode: retCode};
    m_pusher([req.uid], JSON.stringify(resp));
};複製程式碼

這裡由於篇幅不便太大,我刪去了錯誤處理的部分,大概就是把資訊記錄在記憶體裡,稍後我們會用到

狀態邏輯更新

因為我們的同步方式是一種狀態同步,所有的邏輯運算全部在服務端計算,所以我的方案是服務端有一個定時器,每個固定時間伺服器根據當前場上單位的狀態資訊進行一次邏輯更新,包括位置更新,速度衰減,物體碰撞等。然後把最新的狀態資訊同步給客戶端,客戶端收到狀態資訊後通過插值表現出連續的運動軌跡(插值一是因為伺服器協議傳輸可能會由於網路波動而不連續,二是因為客戶端畫面重新整理率60fps會遠高於伺服器邏輯更新頻率25fps)
update方法如下:

/**
 * 固定時間更新,處理邏輯
 */
const onUpdate = function () {
    // dt = 40ms
    const dt = commonConf.updateDt / 1000;
    const battleList = battleData.battleList;
    // 遍歷所有正在進行的所有戰鬥
    for (const battleId in battleList) {
        if (battleList.hasOwnProperty(battleId)) {
            const battle = battleList[battleId];
            (function (battle) {
                // 更新時間
                battle.runTime += 40;
                // 更新座標
                utils.traverse(battle.users, function (userItem) {
                    // 限制最高速度
                    calcSpeedLimit(userItem.speedX, userItem.speedY, userItem.topSpeed);
                    userItem.posX += userItem.speedX * dt;
                    userItem.posY += userItem.speedY * dt;
                    if ((userItem.accX <= 0 && userItem.accY <= 0) || k < 1) {
                        // 如果當前沒有移動或速度超速,速度均勻衰減
                        userItem.speedX *= 0.9;
                        userItem.speedY *= 0.9;
                    }
                });

                // 更新釋放技能
                utils.traverse(battle.users, function (userItem) {
                    if (userItem.skillId != 0) {
                        if (userItem.skillId & 1 > 0) {
                            // 火球
                            battle.addBall(userItem);
                        }
                    }
                });

                // 更新火球移動
                utils.traverse(battle.balls, function (ballItem) {
                    // 已經達到速度上限,去掉加速度
                    // 更新狀態
                    ballItem.posX += ballItem.speedX * dt;
                    ballItem.posY += ballItem.speedY * dt;
                    ballItem.life -= dt;
                });

                // 計算碰撞
                const colliders = {};
                Object.assign(colliders, battle.users, battle.walls, battle.balls);
                utils.traverse(colliders, function (item1) {
                    utils.traverse(colliders, function (item2) {
                        // 碰撞邏輯在下文有介紹
                        checkCollision(battle, item1, item2, dt);
                    });
                });

                // 推送資料
                let users = [];
                let infos = [];
                let ballsInfo = [];
                const resp = {
                    pid: 4,
                    retCode: 0,
                    runTime: battle.runTime,
                    infos: infos,
                    balls: ballsInfo
                };
                if (users.length > 0) {
                    m_pusher(users, JSON.stringify(resp));
                }

                // 清理狀態資料
                // 1. 清理技能釋放狀態
                // 2. 清理爆炸了的火球
                // 3. 清理被擊敗的角色
            })(battle);
        }
    }
};複製程式碼

原文篇幅太長,這裡就只保留重要程式碼,不重要的就全換成註釋了,完整程式碼可以在文末找到。

碰撞計算

由於邏輯全都放在了伺服器端,我們就不能用現成的物理引擎了,那就自己寫一個簡單的碰撞邏輯吧,反正node本身也不適合精確計算,能用就行233
碰撞分為以下幾種

  1. 角色x角色
  2. 角色x牆壁
  3. 角色x火球
  4. 火球x牆壁
  5. 火球x火球
/**
 * 當角色碰撞到角色
 * @param user1
 * @param user2
 * @param dt
 */
const onUserEnterUser = function (user1, user2, dt) {
    // 碰撞到其他角色 - 交換速度,根據動量和能量守恆
    // v1'=((m1-m2)v1+2m2v2)/(m1+m2) v2'=((m2-m1)v2+2m1v1)/(m1+m2)
};

/**
 * 當角色碰撞到牆
 * @param user
 * @param wall
 */
const onUserEnterWall = function (user, wall) {
    // 牆壁法線方向的速度歸零 R'=I-(IN)N
    const N = wall.normal;
    const IX = user.speed;
    const INDot = utils.vector2Dot(I, N);
    const R = I - INDot * N;
    user.speed = R;
    // 修正邊界
    const threshold = wall.width / 2 + user.size;
    const P = user.pos - wall.pos;
    const PNDot = utils.vector2Dot(P, N);
    // pp是N上的投影
    const PP = PNDot * N;
    if (utils.vector2Dot(PP, N) > 0 &&
        utils.vector2Length(PP) < threshold) {
        // 角色到牆裡面去了,修正座標
        const OA = threshold * N;
        const OB = P + OA - PP;
        user.pos = wall.pos + OB;
    }
};

/**
 * 當球碰撞到牆
 * @param ball
 * @param wall
 * @param dt
 */
const onBallEnterWall = function (ball, wall, dt) {
    if (ball.life < 0) {
        // 壽命結束,撞牆爆炸
        ball.explode = true;
        return;
    }
    const N = wall.normal;
    const I = ball.speed;
    // 反射向量R = I - 2(I*N)N
    ball.speed = I - 2 * utils.vector2Dot(I, N) * N;
    // 撞牆去掉加速度
    ball.acc = 0;
    ball.pos += ball.speed * dt;
};

/**
 * 當球碰撞到角色
 * @param ball
 * @param user
 */
const onBallEnterUser = function (ball, user) {
    // 球爆炸
    ball.explode = true;
    user.life -= ball.power;
    const N = utils.vector2Normalize(user.pos - ball.pos);
    // 角色被炸向相反方向,根據球的威力和自身的質量計算新的速度
    user.speed = N * ball.power / user.mass;
};

/**
 * 當球碰撞到球
 * @param ball1
 * @param ball2
 */
const onBallEnterBall = function (ball1, ball2) {
    // 直接爆炸
    ball1.explode = true;
    ball2.explode = true;
};複製程式碼

服務端搭建

客戶端相關準備

客戶端的搭建非常簡單,只要把程式碼clone下來用unity開啟直接執行就行了,當然直接這樣的話會提示伺服器連線失敗www
怎麼搭建伺服器呢,我們就假設伺服器在本地(如果想把伺服器部署在其他機器上的話,找到客戶端Assets/Scripts/Conf/SFCommonConf.cs檔案,把相關IP地址改掉就行了。

安裝node.js

當然Windows是可以安裝node的,不過還是建議在Linux或者macOS下使用,口味更佳~
具體教程就沒有必要貼出來了,大家可以到官網上自行下載。

部署伺服器

Clone遊戲程式碼,進入server/app目錄,執行npm install命令即可一鍵安裝所有依賴(其實就倆,colors和redis)
然後在相同目錄下執行命令node ./,即可啟動伺服器,CTRL+C中斷,因為程式執行在前臺,所以如果是通過ssh操作伺服器的話,可能需要screen之類的配套工具。
伺服器端使用埠19621,請確保該埠沒有被佔用,如果已經佔用了的話會給出提示

1001
1001

然後就可以啟動多個客戶端進行聯機遊玩了。雖然還是有不少的bug,不過主遊戲流程應該是OK...的吧

後記

一開始也沒想著有人看,純粹是自己的學習筆記,不過好像到現在還是有幫到一些人233,順便感謝催更(誤
這個專案算是我轉Unity以來第一個練手的東西,用的框架現在在我們新專案中也在不斷完善,之後心情好的話也許會記錄新遊戲的開發,或者是這個框架的完善也說不定~

完整程式碼

上面貼出的程式碼片段由於篇幅限制只保留了關鍵部分,完整的程式碼可在我的github上找到

相關文章