unity3D - 人物操作(3D視角控制 + 遙桿移動(dòng))

本文基于unity3D開發(fā)一個(gè)MMORPG游戲,闡述一下項(xiàng)目中研究3D自由視角控制 和 搖桿操作人物的經(jīng)驗(yàn)。

整體分為五個(gè)部分:

一、3D視角控制

二、搖桿操作移動(dòng)

三、3D視角結(jié)合搖桿一起操作

四、PC適配

五、多人網(wǎng)絡(luò)同步

一、 3D視角控制(移動(dòng)端)

3D視角控制的核心邏輯 : 根據(jù)用戶滑動(dòng)屏幕的操作,實(shí)時(shí)旋轉(zhuǎn)攝像頭的位置信息。

拆分一下,解決兩件事就行:

1、識(shí)別并獲取用戶滑動(dòng)屏幕的操作

2、實(shí)時(shí)旋轉(zhuǎn)攝像頭的位置信息

先來解決第一件事:如何識(shí)別和獲取用戶滑動(dòng)屏幕的操作?

用戶在屏幕上的是touch事件,這件事比較簡單,直接拿用戶滑動(dòng)屏幕的距離即可,API:Input.GetTouch(0).deltaPosition

有可能多個(gè)手指在屏幕上滑動(dòng),所以這里還必須處理的另一件事是多指觸控,代碼如下:

//雙指觸控管理函數(shù)
    private void TwoFingerControl()
    {
        if (Input.touchCount == 0)
        {
             isInUITouch0 = false;
             isInUITouch1 = false;
        }
        else if (Input.touchCount == 1)
        {
            if (Input.GetTouch(0).phase == TouchPhase.Began)
            {
                OnFingerDown(Input.GetTouch(0).position, 0);
            }
            else if (Input.GetTouch(0).phase == TouchPhase.Moved || Input.GetTouch(0).phase == TouchPhase.Stationary)
            {
               OnFingerMove(Input.GetTouch(0).deltaPosition, 0);
            }
            else
            {
                if (Input.GetTouch(0).phase == TouchPhase.Moved || Input.GetTouch(0).phase == TouchPhase.Stationary)
                {
                    OnFingerMove(Input.GetTouch(0).deltaPosition, 0);
                }
            }
        }
        else if (Input.touchCount >= 2)
        {
            if (Input.GetTouch(0).phase == TouchPhase.Began)
            {
                OnFingerDown(Input.GetTouch(0).position, 0);

            }
            else if (Input.GetTouch(0).phase == TouchPhase.Moved || Input.GetTouch(0).phase == TouchPhase.Stationary)
            {
                    OnFingerMove(Input.GetTouch(0).deltaPosition, 0);
            }

            if (Input.GetTouch(1).phase == TouchPhase.Began)
            {
                OnFingerDown(Input.GetTouch(1).position, 1);
            }
            else if (Input.GetTouch(1).phase == TouchPhase.Moved || Input.GetTouch(1).phase == TouchPhase.Stationary)
            {
                OnFingerMove(Input.GetTouch(1).deltaPosition, 1);
            }
        }
    }

當(dāng)按下的時(shí)候判斷是哪個(gè)手指的序號(hào),確認(rèn)一下是否觸摸在正確的位置上,如果是正確的位置,則將deltaPosition傳入onFingerMove方法做接下來的操作。

接下來解決第二件事:實(shí)時(shí)旋轉(zhuǎn)攝像頭的位置信息

調(diào)用camera的RotateAround方法旋轉(zhuǎn)攝像頭。API:gameCamera.transform.RotateAround

但不能隨意選擇攝像頭,會(huì)引起穿模,所以就需要限制攝像頭的x坐標(biāo)不能超過一定角度,在這個(gè)角度上不會(huì)影響穿模,具體角度可以自行設(shè)定,這里是[10,80]

代碼如下:

//獲取攝像機(jī)初始位置
            Vector3 pos = gameCamera.transform.position;
            //獲取攝像機(jī)初始角度
            Quaternion rot = gameCamera.transform.rotation;

            //攝像機(jī)圍繞player的位置延player的Y軸旋轉(zhuǎn),旋轉(zhuǎn)的速度為鼠標(biāo)水平滑動(dòng)的速度
            gameCamera.transform.RotateAround(player.transform.position, player.transform.up, deltaPosition.x * rotaSpeed);
            //可以設(shè)定人物的方向要不要跟隨轉(zhuǎn)動(dòng)
            //player.transform.RotateAround(player.transform.position, player.transform.up, deltaPosition.x * rotaSpeed);
            //攝像機(jī)圍繞player的位置延自身的X軸旋轉(zhuǎn),旋轉(zhuǎn)的速度為鼠標(biāo)垂直滑動(dòng)的速度
            gameCamera.transform.RotateAround(player.transform.position, gameCamera.transform.right, -deltaPosition.y * rotaSpeed);

            //獲取攝像機(jī)x軸向的歐拉角
            float x = gameCamera.transform.eulerAngles.x;

            //如果攝像機(jī)的x軸旋轉(zhuǎn)角度超出范圍,恢復(fù)初始位置和角度
            if (x < 10 || x > 80)
            {
                gameCamera.transform.position = pos;
                gameCamera.transform.rotation = rot;
            }

把以上代碼放到update()函數(shù)體中調(diào)用,即可完成3D視角控制的事情。

二、 搖桿操作移動(dòng)(移動(dòng)端)

搖桿操作移動(dòng)的核心原理:制作一個(gè)搖桿按鈕,記錄用戶觸摸操作搖桿的實(shí)時(shí)位置,從而控制人物的方向和位置信息。

第一件事:制作一個(gè)搖桿按鈕,記錄用戶觸摸搖桿的實(shí)時(shí)位置

1、用兩張圖疊加一下,掛載到一個(gè)GameObject上,取名joystick

image.png

2、在這個(gè)物體上增加兩個(gè)Event Trigger事件,分別是Drag和End Drag,讓按鈕可以拖動(dòng)事件返回 和 結(jié)束拖動(dòng)的事件返回


image.png

3、綁定一個(gè)joystick的腳本,處理這兩個(gè)事件,拿到用戶觸摸搖桿的實(shí)時(shí)位置信息

//拖動(dòng)
public void on_stick_drag() {
    Vector2 pos = Vector2.zero;
    //多指的判斷就不再這里貼了
    RectTransformUtility.ScreenPointToLocalPointInRectangle(this.transform as RectTransform, Input.GetTouch(0).position, this.cs.worldCamera, out pos);
    float len = pos.magnitude;
        if (len <= 0) {
            this.touch_dir = Vector2.zero;
            return;
        }

        // 歸一化處理,方便后續(xù)做其他邏輯
        this.touch_dir.x = pos.x / len; // cos(r)
        this.touch_dir.y = pos.y / len; // (sinr) cos^2 + sin ^ 2 = 1;
        if (len >= this.max_R) { // this.max_R / len = x` / x = y` / y;
            pos.x = pos.x * this.max_R / len;
            pos.y = pos.y * this.max_R / len;
        }
        this.stick.localPosition = pos;
}
//結(jié)束拖動(dòng)
public void on_stick_end_drag() {
    this.stick.localPosition = Vector2.zero;
    this.touch_dir = Vector2.zero;
}

這樣通過這兩個(gè)事件觸發(fā)就拿到了拖動(dòng)和結(jié)束拖動(dòng)的信息,并且將拖動(dòng)的坐標(biāo)歸一化處理。

第二件事:根據(jù)拖動(dòng)的歸一化數(shù)據(jù),控制人物移動(dòng)

private void onJoyStickEvent(string uname, object udata) {
        if (this.state != (int)RoleState.Idle && this.state != (int)RoleState.Run) {
            return;
        }
        if (dir.x == 0 && dir.y == 0) {
            this.setState((int)RoleState.Idle);
            return;
        }

        this.setState((int)RoleState.Run);

        float vx = this.speed * dir.x;
        float vz = this.speed * dir.y;

        float dt = Time.deltaTime;
        Vector3 pos = this.transform.position;
        pos.x += (vx * dt);
        pos.z += (vz * dt);
        if (!this.astar.isWordPosCanGo(pos)) {
            return;
        }

        this.transform.LookAt(pos);
        this.transform.position = pos;
    }

拿到歸一化的x和y坐標(biāo),乘以速度,然后更新玩家的朝向和位置信息。

到這里,3D視角控制和搖桿操作這兩部分就完成了,但有個(gè)很大的問題,目前不能一個(gè)手指控制搖桿,一個(gè)手指控制視角,所以接下來看第三部分。

三、 3D視角結(jié)合搖桿一起操作

這個(gè)雙指同時(shí)操作的核心原理:先判斷攝像頭旋轉(zhuǎn)后的方向信息,然后將搖桿移動(dòng)的坐標(biāo)信息旋轉(zhuǎn)一個(gè)角度得到正確的移動(dòng)坐標(biāo)位置。

先判斷哪個(gè)手指是搖桿,哪個(gè)手指是視角控制。這件事就不闡述了

來看下核心點(diǎn):如何判斷攝像頭旋轉(zhuǎn)后的方向信息,然后將移動(dòng)的坐標(biāo)信息旋轉(zhuǎn)一個(gè)角度得到正確的移動(dòng)坐標(biāo)位置。

從數(shù)學(xué)上來講,就是已知圓心(0,0)的情況下,將一個(gè)點(diǎn)(x,y),旋轉(zhuǎn)一個(gè)角度,得到一個(gè)新的點(diǎn)(x1,y1)

具體介紹可以看這篇文章:https://www.zhihu.com/question/58468471

公式是:

/** 旋轉(zhuǎn)公式
* x1=xcosθ-ysinθ
* y1=xsinθ+ycosθ
*/

直接上代碼:

//因?yàn)槲业臄z像頭是順時(shí)針旋轉(zhuǎn)的,這個(gè)公式求的是逆時(shí)針旋轉(zhuǎn)的新位置,所以需要將攝像頭順時(shí)針轉(zhuǎn)向的角度 轉(zhuǎn)換成 逆時(shí)針轉(zhuǎn)向的角度,順時(shí)針旋轉(zhuǎn)一個(gè)角度就等于逆時(shí)針旋轉(zhuǎn)360減去這個(gè)角度
        float rotateAngel = 360 - gameCamera.transform.eulerAngles.y;
        //計(jì)算一個(gè)點(diǎn) 逆時(shí)針旋轉(zhuǎn)了rotateAngel角度之后,得到的新點(diǎn)坐標(biāo)
        //這個(gè)意思是,攝像機(jī)旋轉(zhuǎn)一個(gè)角度之后,要將人物操作桿的坐標(biāo)重定向到新方向的坐標(biāo)位置
        /** 旋轉(zhuǎn)公式
         * x1=xcosθ-ysinθ
         * y1=xsinθ+ycosθ
         */
        float x1 = dir.x * Mathf.Cos(rotateAngel * Mathf.Deg2Rad) - dir.y * Mathf.Sin(rotateAngel * Mathf.Deg2Rad);
        float y1 = dir.x * Mathf.Sin(rotateAngel * Mathf.Deg2Rad) + dir.y * Mathf.Cos(rotateAngel * Mathf.Deg2Rad);
        dir.x = x1;
        dir.y = y1;

這樣的話,就搞定了搖桿移動(dòng)和視角控制混合操作了。

四、 PC適配

簡單說一下PC適配的兩個(gè)邏輯,移動(dòng)控制是將AWSD按鍵換算成搖桿操作的4個(gè)歸一化方向,分別是(0,1),(1,0),(0,-1),(-1,0)

代碼如下:

if (Input.GetKey(KeyCode.W))
        {
            EventMgr.Instance.Emit("JoyStick", new Vector2(0, 1));
        }
        else if (Input.GetKey(KeyCode.S))
        {
            EventMgr.Instance.Emit("JoyStick", new Vector2(0, -1));
        }
        else if (Input.GetKey(KeyCode.D))
        {
            EventMgr.Instance.Emit("JoyStick", new Vector2(1, 0));
        }
        else if (Input.GetKey(KeyCode.A))
        {
            EventMgr.Instance.Emit("JoyStick", new Vector2(-1, 0));
        }
        //如果鼠標(biāo)抬起來,就取消人物的動(dòng)作
        if (Input.GetKeyUp(KeyCode.W) || Input.GetKeyUp(KeyCode.S) || Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.D))
        {
            EventMgr.Instance.Emit("JoyStick", new Vector2(0, 0));
        }

另一個(gè)邏輯是,視角控制是將移動(dòng)端的touch事件轉(zhuǎn)換成PC的鼠標(biāo)事件,用MouseX代替touch.deltaPos其他邏輯是一致的

代碼如下:

private void RotateView()
    {
        #if UNITY_EDITOR || UNITY_STANDALONE_WIN
        if (Input.GetMouseButtonDown(1))
        {
            isRotaing = true;
        }
        if (Input.GetMouseButtonUp(1))
        {
            isRotaing = false;
        }
        if (isRotaing)
        {
            //獲取攝像機(jī)初始位置
            Vector3 pos = gameCamera.transform.position;
            //獲取攝像機(jī)初始角度
            Quaternion rot = gameCamera.transform.rotation;

            //攝像機(jī)圍繞player的位置延player的Y軸旋轉(zhuǎn),旋轉(zhuǎn)的速度為鼠標(biāo)水平滑動(dòng)的速度
            gameCamera.transform.RotateAround(player.transform.position, player.transform.up, Input.GetAxis("Mouse X") * rotaSpeed);
            player.transform.RotateAround(player.transform.position, player.transform.up, Input.GetAxis("Mouse X") * rotaSpeed);
            //攝像機(jī)圍繞player的位置延自身的X軸旋轉(zhuǎn),旋轉(zhuǎn)的速度為鼠標(biāo)垂直滑動(dòng)的速度
            gameCamera.transform.RotateAround(player.transform.position, gameCamera.transform.right, -Input.GetAxis("Mouse Y") * rotaSpeed);

            //獲取攝像機(jī)x軸向的歐拉角
            float x = gameCamera.transform.eulerAngles.x;

            //如果攝像機(jī)的x軸旋轉(zhuǎn)角度超出范圍,恢復(fù)初始位置和角度
            if (x < 10 || x > 80)
            {
                gameCamera.transform.position = pos;
                gameCamera.transform.rotation = rot;
            }

        }
        cameraOffset = gameCamera.transform.position - player.transform.position;//更新
#elif UNITY_ANDROID
        TwoFingerControl();
        cameraOffset = gameCamera.transform.position - player.transform.position;//更新    
#endif
    }

五、 多人網(wǎng)絡(luò)同步(狀態(tài)同步)

多人網(wǎng)絡(luò)移動(dòng)同步涉及前后端通信,不是本文重點(diǎn)

簡單描述下狀態(tài)同步流程:

1、客戶端將joystick拿到的觸摸信息歸一化,加上玩家攝像頭轉(zhuǎn)向信息合并,就是第三部分的混合計(jì)算出來的轉(zhuǎn)向+位置信息,發(fā)送給后臺(tái)

2、后臺(tái)根據(jù)客戶端的這些信息來計(jì)算這個(gè)用戶的坐標(biāo)位置(可以設(shè)置處理間隔,比如1秒處理20次,就是50ms一次計(jì)算,根據(jù)后臺(tái)處理能力和客戶端位置同步幀率來平衡這個(gè)間隔)

3、后臺(tái)計(jì)算得到的坐標(biāo)發(fā)送給客戶端,客戶端根據(jù)這個(gè)位置調(diào)整玩家的朝向和位置。

這里面有個(gè)點(diǎn),實(shí)際上后臺(tái)只計(jì)算玩家的朝向和位置,客戶端攝像頭的3D視角的角度是不用后臺(tái)關(guān)心的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容