---
# System prepended metadata

title: Untitled

---

魏琛棣大程报告
各模块实现
3.4.1 坦克驾驶模块
3.4.1.1 功能介绍
实现基于物理的坦克移动，通过wasd或者方向键控制坦克的加速减速和方向控制，使用空格实现刹车，使用R键实现坦克的加速，并同时实现坦克运动时的细节动画。
3.4.1.2 prefabs
坦克的prefab截图：
![](https://i.imgur.com/vse2GQx.png)
在坦克的每一个轮子上添加组件WheelCollider，通过使用WheelCollider控制其属性值为坦克提供前进的动力和阻力，同时通过控制车轮的转速来实现通过两侧履带的速度差控制坦克的运动方向。
3.4.1.3 脚本
通过控制wheelcollider的motorTorque和brakeTorque属性来为坦克提供动力和阻力。
```
    public void ApplyMotorTorque(float torque)
    {
        if (OverTorque())
            torque = 0f;
        wheelCollider.motorTorque = torque;
    }
    public void ApplyBrakeTorque(float brake)
    {
        wheelCollider.brakeTorque = brake;
    }
```
通过控制贴图的位置来实现履带的运动动画。
```
    void TrackAnimation()
    {
        if (_leftTrackMesh && _rightTrackMesh)
        {
            _leftTrackMesh.material.SetTextureOffset("_MainTex", new Vector2((wheelColliders_L[Mathf.CeilToInt((wheelColliders_L.Count) / 2)].wheelRotation / 1000f) * trackScrollSpeedMultiplier, 0));
            _leftTrackMesh.material.SetTextureOffset("_BumpMap", new Vector2((wheelColliders_L[Mathf.CeilToInt((wheelColliders_L.Count) / 2)].wheelRotation / 1000f) * trackScrollSpeedMultiplier, 0));
            _rightTrackMesh.material.SetTextureOffset("_MainTex", new Vector2((wheelColliders_R[Mathf.CeilToInt((wheelColliders_R.Count) / 2)].wheelRotation / 1000f) * trackScrollSpeedMultiplier, 0));
            _rightTrackMesh.material.SetTextureOffset("_BumpMap", new Vector2((wheelColliders_R[Mathf.CeilToInt((wheelColliders_R.Count) / 2)].wheelRotation / 1000f) * trackScrollSpeedMultiplier, 0));
        }
    }
```
通过射线检测车轮距离道路的高度实现车轮的起伏定位。
```
if (Physics.Raycast(ColliderCenterPoint, -wheelCollider.transform.up, out hit, (wheelCollider.suspensionDistance + wheelCollider.radius) * 
            transform.localScale.y) && !hit.transform.IsChildOf(_tankController.transform) && !hit.collider.isTrigger)
        {
            // Assigning position of the wheel if we have hit.
            wheelModel.transform.position = hit.point + (wheelCollider.transform.up * wheelCollider.radius) * transform.localScale.y;
            float extension = (-wheelCollider.transform.InverseTransformPoint(CorrespondingGroundHit.point).y - wheelCollider.radius) / 
                wheelCollider.suspensionDistance;
            Debug.DrawLine(CorrespondingGroundHit.point, CorrespondingGroundHit.point + wheelCollider.transform.up * (CorrespondingGroundHit.force / _rigid.mass), extension <= 0.0 ? Color.magenta : Color.white);
            Debug.DrawLine(CorrespondingGroundHit.point, CorrespondingGroundHit.point - wheelCollider.transform.forward * 
                CorrespondingGroundHit.forwardSlip * 2f, Color.green);
            Debug.DrawLine(CorrespondingGroundHit.point, CorrespondingGroundHit.point - wheelCollider.transform.right * 
                CorrespondingGroundHit.sidewaysSlip * 2f, Color.red);
        }
        else
        {
            // Assigning position of the wheel to default position if we don't have hit.
            wheelModel.transform.position = ColliderCenterPoint - (wheelCollider.transform.up * wheelCollider.suspensionDistance) * transform.localScale.y;
        }
```
通过检测wheelCollider角度，并同步车轮mesh模型角度与wheelCollider同步实现车轮转动动画。
```
// X axis rotation of the wheel.
        wheelRotation += wheelCollider.rpm * 6 * Time.deltaTime;

        // Assigning rotation of the wheel (X and Y axises).
        wheelModel.transform.rotation = wheelCollider.transform.rotation * Quaternion.Euler(wheelRotation, 
            wheelCollider.steerAngle, wheelCollider.transform.rotation.z);
        trackBone.transform.position = new Vector3(trackBone.transform.position.x,  
            wheelModel.transform.position.y - transform.localScale.y / 2 + offset, trackBone.transform.position.z);

```

3.4.1.4 使用步骤
将TankController挂载在Tank本体上，在每一个车轮的位置实例化空物体，并挂载WheelCollider脚本，调整其参数至符合车轮要求，并在该空物体上挂载TankWheelCollider脚本。
在游戏场景中，通过按键wasd或者上下左右控制坦克，w或上键控制坦克加速，s或下键控制坦克减速，a或左键控制坦克左转，d或右键控制坦克右转。长按R键控制坦克加速，松开R键坦克逐渐恢复正常速度，长按空格键坦克迅速减速刹车至静止。

3.4.2 摄像机第三人称视角控制模块
3.4.2.1 功能介绍
摄像机始终以坦克为中心环绕转动，坦克主体不离开摄像机中心部位，保证坦克始终处于摄像机正前方。
3.4.2.2 prefabs
无prefab描述
3.4.2.3 脚本
通过CameraController实现控制摄像机的第三人称视角功能。通过获取鼠标的移动输入，得到摄像机即将移动的距离大小，将orbitX和orbitY两个参数与之关联作为摄像机的旋转参数，使其始终朝向坦克。摄像机的位置通过坦克的位置加上预设的摄像机坦克距离，并将计算得到的摄像机角度的四元数与相对位置相乘，得到摄像机的距离。

```
private void Inputs()
    {
        float mouseX = Input.GetAxis("Mouse X") * movespeed;
        float mouseY = Input.GetAxis("Mouse Y") * movespeed;
        orbitX += mouseX * Time.deltaTime;
        orbitY -= mouseY * Time.deltaTime;
    }

    private void Orbit()
    {
        orbitY = Mathf.Clamp(orbitY, minOrbitY, maxOrbitY);

        orbitRotation = Quaternion.Euler(orbitY, orbitX, 0);
        orbitPosition = orbitRotation * new Vector3(0f, distanceUp, -distanceAway) + target.position;

        transform.rotation = orbitRotation;
        transform.position = orbitPosition;
    }
```

3.4.2.4 使用步骤
将CameraController挂载在摄像机上即可运行脚本。在使用过程中，鼠标可以自由移动，而摄像机始终朝向坦克。

3.4.3 副炮控制模块
3.4.3.1 功能介绍
副炮始终朝向UI上的准星方向，可以通过按下鼠标右键进行zoomin瞄准，并限制其转动角度，防止副炮出现穿模或不和谐画面。通过按下鼠标左键进行射击。
3.4.3.2 prefabs
无prefab描述
3.4.3.3 脚本
通过脚本SecondaryArtilleryController实现对副炮的控制。从摄像机发射一条射线，并检测射线与世界发生的碰撞点，如果没有检测到碰撞点则在射线方向一定距离上设置伪碰撞点，控制坦克副炮始终朝向该碰撞点，即实现了坦克副炮受鼠标控制转动效果。
```
    private void GetTargetPoint()
    {
        ray = Camera.main.ScreenPointToRay(ScreenCenterPoint);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 200))
        {
            _tarPoint = hit.point;
        }
        else
        {
            _tarPoint = mainCamera.transform.position + ray.direction * 200;
        }
    }
```
Zoomin效果通过控制摄像机参数fieldOfView，即可实现摄像机的画面放大功能。通过对该值进行插值实现视野的逐渐变化。
```
    private void ZoomIn()
    {
        _cnt += 5 * Time.deltaTime;
        _cnt = Mathf.Min(1.0f, _cnt);
        curCamView = Mathf.Lerp(zoomOutView, zoomInView, _cnt);
        Camera.main.fieldOfView = curCamView;
    }
```
通过在鼠标左键按下时对副炮的目标朝向点添加随机的移动，来实现副炮开枪时的抖动效果，且随机移动的长度受到目标朝向点到副炮枪口距离的影响，保证抖动的一致性。
```
    private void GunShaking()
    {
        float distance = Vector3.Distance(TarPointControl, transform.position);
        float p1, p2, p3;
        p1 = Random.Range(-(distance * _shakePara), (distance * _shakePara));
        p2 = Random.Range(-(distance * _shakePara), (distance * _shakePara));
        p3 = Random.Range(-(distance * _shakePara), (distance * _shakePara));
        TarPointControl = TarPointControl + new Vector3(p1, p2, p3);
    }
```
3.4.3.4 使用步骤
在Tank上找到副炮的GameObject，并将脚本SecondaryArtilleryController挂载在该物体上即可在游戏中实现对副炮的操控。在游戏中，转动鼠标可以控制副炮的朝向，长按右键可以控制屏幕瞄准，长按左键可以控制副炮连续射击。

3.4.4 网络底层模块
3.4.4.1 功能介绍
使用LiteNetLib网络开发插件，使用可靠的UDP传输协议，实现游戏内容的状态同步。通过设置客户端与服务端的方式，客户端向服务端传递操作，服务端处理操作为游戏效果，将处理后的结果返回给客户端，进而实现在画面与内容上客户端与服务端的同步。
3.4.4.2 prefabs
无prefab描述
3.4.4.3 脚本
游戏开始时，服务端初始化NetManager，并开始监听连接请求，客户端在建立自己的NetManager后，调用Connect函数和指定的ip地址与端口号的服务端进行连接，实现客户端与服务端的连接功能。
```
    // 服务端函数
    private void StartServer()
    {
        _isServer = true;
        _connected = true;
        // connectKey有待研究
        _serverNetManager = new NetManager(this, 10, "");
        if (_serverNetManager.Start(_port))
        {
            Debug.Log("服务端开启成功!");
        }
        else
        {
            Debug.LogError("服务端开启失败!");
        }
    }

    // 客户端函数
    private void ConnectServer()
    {
        _isServer = false;
        _clientNetManager = new NetManager(this, "");
        if (_clientNetManager.Start())
        {
            _clientNetManager.Connect(_ip, _port);
            Debug.Log("客户端开启成功!");
        }
        else
        {
            Debug.Log("客户端开启失败!");
        }
    }
```
游戏运行时，在每一帧调用函数PollEvents，该函数将每一帧网络接收到的信息按照信息的类型分别进行处理，包括OnNetworkError，OnNetworkLatencyUpdate，OnNetworkReceive，OnNetworkReceiveUnconnected，OnPeerConnected，OnPeerDisconnected。我们分别在对应的函数中处理相应的信息即可。
网络的传输依赖于数据包，我们对于每一种需要同步的内容进行数据包的定义，每一个类为一个数据包的一部分，类定义在PackageClass中，并通过枚举类型记录该类的信息，并对于客户端与服务端的类进行分类处理。使用SetPackageType函数实现按照类型转换数据包，保证程序的复用性。数据包的格式为序列化对象长度+对象类型+有效载荷。
当客户端或者服务端接收到新的数据包后，会根据上述数据包格式对数据包进行解析，将其解析为可以读取的格式，并传递给上层函数处理。
当上层应用想要发送数据包时，需要将自己的信息进行实例化后，调用AddS2CPackage或AddC2SPackage函数，将数据包进行发送操作。
```
public void AddC2SPackage(ref object package, C2SPackageTypes type, bool directSend, SendOptions sendOptions = SendOptions.Sequenced)
    {
        AnalyseC2SPackageType.Instance.SetPackageType(ref package, type);
        byte[] serializedPackage;
        Serialize(package, out serializedPackage);
        int length = serializedPackage.Length;
        Debug.Log(length);
        if (length + 4 + _dataWriter.Length > 505)
        {
            SendStatus(sendOptions);
        }
        _dataWriter.Put((int)type);
        _dataWriter.Put(length);
        _dataWriter.Put(serializedPackage);
        if (directSend)
        {
            SendStatus(sendOptions);
        }
    }
```
3.4.4.4 使用步骤
将Network脚本挂载在场景中的NetworkManager空物体上，当需要发送数据包的时候，根据自己需要发送的数据类型选择合适的类，实例化出对象，将发送的数据存入实例中，并将实例转换为object类型，接下来调用函数AddC2SPackage或AddS2CPackage即可发送数据包。
在函数ProcessFromClientPackage和函数ProcessFromServerPackage中，分别根据自己定义的类的类型，为对应类型的数据包添加调用处理函数，即可在客户端或服务端接收到数据包后调用对应函数进行解析操作。

5.心得体会
本课程作业是一个相对较大的项目，并且涉及到了游戏开发中的各个方面，包括网络、渲染、AI、游戏逻辑、物理等等，对于本组的每一位同学都是一个很大的锻炼，我也在这项作业的工作中学习到了很多。在这次的作业中我们也学到了很多游戏开发中所必不可少的知识，感受到了团队在开发中的流程与工作，也在其中总结到了难得的经验教训。我相信这一个游戏项目一定可以在未来的学习、面试乃至工作中为我提供宝贵的经验。

Reference:
1. Unity LiteNetLib Multiplayer Basics and .NET Core Server 
Author Ömer Faruk Dogan
Link:https://www.udemy.com/unity-udp-with-litenetlib-building-a-simple-movement-game/#instructor-1

2. https://blog.csdn.net/sumkee911/article/details/54908292

3. https://github.com/RevenantX/LiteNetLib









