内容纲要
  • alt + 鼠标拖拽 视角旋转

  • 按住鼠标中间,并移动鼠标,可以平移视图

  • 按住鼠标右键,wasdqe 可以在6个方向上移动

  • 在 Hierarchy视图中,鼠标左键先选中一个 物体,按下 shift ,再 鼠标左键选中另外一个 物体,即可选中 多个 物体,进行操作。此时把在右侧Inspector添加组件就可为所有GameObject添加Component

  • unity默认游戏单位为米

  • plane默认长宽是10米 Capsule默认是2个1米的半球加一个1m的圆柱体组成 cube Sphere

  • unity中的Skybox也会提供一些天光(各个方向上微弱的光)

  • Global坐标系永远和场景的坐标系保持一致 (随着世界坐标系选择)

  • local坐标系总是与物体自身方向保持平行

  • persp是透视模式,近大远小

  • Iso是平行投影

  • materials 材质

  • 灯光的颜色与物体的颜色向乘得到最后显示给用户的颜色(颜色分量相乘,物体之所以显示红色是因为其反射红色的光)

  • Time.timeScale //时间流逝的规模。这可以用于慢动作效果

  • 当timeScale为1.0时,时间与实时一样快。当timeScale为0.5时,时间比实时慢2倍。

  • 当timeScale设置为零时,如果所有功能都与帧速率无关,则游戏基本暂停。

  • 除了realtimeSinceStartup,timeScale影响Time类的所有时间和delta时间测量变量。

  • 如果降低timeScale,Time.fixedDeltaTime也会降低相同的量。设置为零时,不会调用- fixedupdate

  • 小球移动:

    private void FixedUpdate()
    {
        Time.timeScale =0.5f;      //子弹时间,加速减速均可用该值实现
        if (Input.GetKey(KeyCode.LeftArrow))            //unity事先就检测到了所有按键,所以在这可以直接判断按键是否按下,用if判断就可以同时施加不同方向上的力   //GetKey是检测按键的持续情况,GetKeyUp和GetKeyDown是只有在按键的上、下边缘才会检测,可以做触发器,但不宜实时控制运动
        {
            rigidbody.AddForce(-moveForce * Time.fixedDeltaTime, 0, 0);   //乘以FixedDeltaTime的意义就在于时间可以缩放,加了个权重
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            rigidbody.AddForce(moveForce * Time.fixedDeltaTime, 0, 0);
        }
        if (Input.GetKey(KeyCode.UpArrow))
        {
            rigidbody.AddForce(0, 0, moveForce * Time.fixedDeltaTime);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            rigidbody.AddForce(0, 0, -moveForce * Time.fixedDeltaTime);
        }
        if (Input.GetKey(KeyCode.Space))
        {
            rigidbody.AddForce(0, jumpForce * Time.fixedDeltaTime, 0);
        }
    }
  • Physic Material //物理学材料是用来调整摩擦和碰撞对象的反弹效应。 //属于collider一个属性

  • 属性: 功能:

  • 动态摩擦 已经移动时使用的摩擦力。通常是从0到1的值。零值感觉像冰一样,值1将使它非常快地停下来,除非大量的力或重力推动物体。

  • 静摩擦力 物体静止在表面上时使用的摩擦力。通常是0到1之间的值。零值感觉像冰,值1会使对象移动变得非常困难。

  • 反弹力 表面有多么有弹性?值0不会反弹。值1将在没有任何能量损失的情况下反弹,但是可以预期某些近似值,但这可能会为模拟增加少量能量。

  • 摩擦组合 如何组合两个碰撞物体的摩擦力。
    – 平均 两个摩擦值是平均值。
    – 最低 使用这两个值中最小的一个。
    – 最大 使用这两个值中最大的一个。
    – 乘以 摩擦值相互相乘。

  • 弹跳组合 如何组合两个碰撞对象的弹性。它具有与摩擦组合模式相同的模式

  • 摩擦力是防止表面相互滑落的量。尝试堆叠对象时,此值很重要。摩擦有两种形式,动态和静态。物体静止时使用静摩擦力。它会阻止对象开始移动。如果对物体施加足够大的力,它将开始移动。此时动态摩擦将发挥作用。动态摩擦力现在会在与另一个物体接触时尝试减慢物体的速度。

  • 当两个物体接触时,根据所选择的模式对它们两者施加相同的弹性和摩擦效果。当两个接触的碰撞器具有不同的组合模式设置时,存在一种特殊情况。在这种特殊情况下,使用具有最高优先级的功能。优先顺序如下:平均 < 最小 < 乘以 < 最大值。例如,如果一个材质具有平均值但另一个具有最大值,则要使用的组合函数为最大值,因为它具有更高的优先级。 //力的作用是相互的,所以当两个物体的摩擦力系数不同时,有不同的计算摩擦力的方法,但是加在这两个物体上的摩擦力大小相等方向相反

  • 让摄像机跟着小球动,如果作为子节点,会收到小球移动和旋转的影响,我们不希望收到旋转影响,所以我们只需要摄像机跟随小球的位置移动就可: //一个平行四边形的移动

public class MainCamera : MonoBehaviour
{
    public Transform player;

    private Vector3 offset;
    private Vector3 playerStartPosition;
    private Vector3 cameraStartPosition;
    // Start is called before the first frame update
    void Start()
    {
        playerStartPosition = player.position;
        cameraStartPosition = this.transform.position;

        offset = this.transform.position - player.position;
    }

    // Update is called once per frame
    void Update()
    {
   //     this.transform.position = cameraStartPosition + (player.position - playerStartPosition);            //用Offset和相机起始位置偏移均可,因为是个平行四边形
        this.transform.position = player.position + offset;
    }
}
  • 为了不影响小球在吃道具时的速度,需要把道具设置为trigger

  • collider性能优化: //collider相当于mesh的网格模型,只不过不是用来画图而是用来计算碰撞的,所有的静态物体生成一个collider静态网格,当有动态物体碰撞到静态物体时,根据该网格计算动态物体该如何进行反应

  • unity中计算场景中所有的静态碰撞器的碰撞体并把他们保持在缓冲中,这使得所有的静态碰撞体不能够移动并节省每帧计算碰撞体的开销 //unity保存到缓存后,不会每帧都再次计算静态碰撞体

  • 我们的错误在于让道具旋转了

  • 任何时候,当我们移动、旋转、缩放一个静态碰撞器时,unity会再次重新计算所有的静态碰撞器,并更新静态碰撞器的缓存,而重新计算缓存是有开销的

  • 我们可以移动、旋转或缩放动态碰撞器并且unity不会重新缓存任何碰撞器的碰撞体 //动态碰撞器实时计算

  • unity鼓励我们移动碰撞体,只要我们在移动之前声明该碰撞器是动态的即可

  • 要做到这点,我们只要使用Rigidbody组件,任何带有Collider和Rigidbody的游戏对象都被认为是动态的,任何不带rigidbody而带有Collider的组件的游戏对象都被认为是静态的,而我们的道具就被认为是静态的,因此unity会每帧重新计算静态碰撞器的缓存

  • 解决方案:

  • 为道具添加Rigidbody,其实变为动态碰撞器,

  • 此时立方体会掉落,因为Rigidbody是有重力的,我们可以设置rigidbody.useGravity = false,这会阻止立方体下落,但是此时立方体虽然不受重力了,但是任然会受其他类型物理力的影响且会带来物理开销

  • 更好的解决方案为设置:Rigidbody.IsKinemaitc = true;

  • 一个Kinematic的刚体不受物理力的影响,而只受图形变换的影响(如动画和Transform组件)
    这一点用在带有触发器或碰撞器的物体上很好,如电梯或平台的自动移动就是用图形动画(而非物理运算)实现的

  • 总结:

    • 静态物体不应具有Rigidbody
    • 动态物体应具有Rigidbody
    • 受物理力影响的Rigidbody应使用默认设置(Kinematic为false)
    • 受图形变换影响的Rigidbody应设置Kinematic为true;

UNET

  • socket通信 http 80端口

  • 玩家拾取物品到背包:

  • C: 拾取物品(玩家ID,物品ID)

  • S:

    • 1、判断物品是否可以拾取,不可拾取则返回对应消息给客户端
    • 2、判断背包是否满,满则返回对应消息给客户端
    • 3、将背包物品存入该玩家在服务器中的背包数据表中(//)
    • 4、给客户端发送消息,添加背包物品(物品id)
    • C:接收消息,解析消息中的消息编号,并判断如果时背包物品的消息编号,就调用对应处理程序
  • Network:

    • NetworkAnimator 游戏角色动画同步
    • NetworkIdentity 游戏场景(Hierarchy)中在网络中需要一个唯一标识
    • NetworkLobbyManager 游戏大厅管理(建立房间,匹配) //Lobby 大厅
    • NetworkLobbyPlayer 游戏大厅中的玩家
    • NetworkManager 简单的网络管理
    • NetworkManagerHUD 一个链接匹配的显示 //HUD 投屏显示
    • NetworkMigrationManager 主机迁移
    • NetworkProximityChecker 范围检测
    • NetworkStartPosition 进入游戏的起始位置
    • NetworkTransform 同步Transform组件的变化
    • NetworkTransformChild 同步Tranform组件子节点的变化
  • 步骤:

  • 新建一个Empty:NetworkController,为其添加NetworkManager组件 //NetworkManager负责玩家连接,但是却没有连接的界面,连接界面由NetworkManagerHUD提供

  • 将Player对象放入Prefeb,并添加NetworkIdentifier组件,并勾选Local Authentication(允许客户端玩家控制此对象,否则对小球调用AddForce无效)

  • 指定NetworkManager的Player Prefab,用于生成Player

  • 为NetworkController添加NetworkManagerHUD,用于表示网络连接界面

  • 为Player添加一个NetworkTranform(用于对象同步),并同步到Prefeb

  • 修改PlayerConntroller脚本:

  • 使其从NetworkBehaviour继承 //所有网络相关的类都从NetWorkBehaviour继承,NetworkBehaviour本身继承MonoBehaviour //但是在2019版本被弃用了

  • 用NetworkBehaviour.isLocalPlayer判断是网络玩家,还是本地玩家,如果是网络玩家就不做脚本处理

  • 添加NetworkManagerHUD后在界面出现的场景:

  • LAN Host 表示你希望成为一个主机

  • LANClient 表示你希望成为个Client,后面填写的是连接的服务器地址 //local host: 127:0:0:1

  • LAN server Only 表示只作为服务端存在

  • 把Player做成预制体,并在场景中删除该角色,让游戏开始后由网络端创建; 为Player添加NetworkIdentity(网络标识):

  • Server Only 表示只在服务器中存在

  • Local player Authority 表示是否是玩家可以操纵的对象 //是否允许该对象给服务器发送消息

  • 添加完NetworkIdentity后就可以把该预制体放到netWorkManager -> Spawn Info -> player Prefab中了

  • 问题:在Host上,自己控制的小球和网络上其他玩家的小球都能被Host控制,且客户端看不见Host移动小球产生的变化

  • 问题解决方案:需要一个网络位置同步组件,就是NetWorkTransform,添加到Player上:

  • Network Send Rate 表示每秒同步几帧画面,王者荣耀是每秒同步15帧

  • Transform sync med 同步的方式 //sync Transform 根据Transform组件同步,//sync Rigidbody 3D 根据物理组件进行同步

  • 在添加NetworkTansform后,所有的player在启动时都会有Network Information的信息,其中仅有一个player的Network Information中的is Local Player是true的,这个就是我们控制的player,只有这个player上绑定的脚本具有向服务器发送位置,同步消息的权限

  • 无权限的player为什么对它在本地进行移动无效了呢? 其实不是无效,我们把镜头拉近会移动,会发现小球会有轻微抖动,这是因为在小球移动后,收到了另一个client(实际控制这个小球的玩家)的networkTransform组件的同步消息,又把小球拉回到了原来的位置 //所以需要在控制脚本中加上判断如果不是本地玩家,直接return,不做处理:

        if (!isLocalPlayer)
            return;
  • 创建游戏大厅:使用NetworkLobby的资源包 (Asset Store): //在资源包的perfab中有LobbyManager,这个就是大厅的预制体 //有了这个组件就可以不需要Main场景中的NetWorkManager和NetworkHUD了

  • 把相应场景设置到Build列表中(某些情况下,build了场景之后没有响应,可能就是场景没有载入),然后在LobbyMananger的脚本中设置Lobby和play的Scene,最大和最小的玩家数量 //很方便的一个预制体 //这个要求Play Scene中需要一个GameRoot的节点,所以我们给他创建一个,然后把所有东西放到这下面

  • 之后我们发现启动场景之后,不会有小球下来,这是因为我们没有把我们制作的Player prefab给到Lobby Manager; 在Lobby Manager中 Lobby Player 和 Game Player是两个概念,Lobby Player在Lobby Manager中已经被指定了,我们需要做的就是指定Game Player,打开Lobby Manager自带的Game Player,你就会发现它带了一个Network Identity和Game Player的脚本(ps:这个Game Player的脚本可以指定玩家的姓名和颜色),所以在我们的player预制体上,我们也需要包含这两个Component;

  • Game Player脚本中的Sync Name勾选后就是游戏大厅中设置的名字,Sync Color勾选后再游戏大厅中设置的颜色也会被保存到player的Material上

  • Lobby Manager中的倒计时的时间可以在Lobby Manager的脚本中的Unity UI Lobby -> Prematch Countdown中进行设置

  • 设置小球的初始位置: //先设置好一些初生点,等全部玩家加载完毕后,设置玩家在初生点出生,问题在于如何同步

  • NetworkBehaviour的类也会像MonoBehaviour类一样通过反射调用方法:

        public virtual void OnStartClient();
        public virtual void OnStartLocalPlayer();
        public virtual void OnStartServer();
  • 但是我们不使用这些方法,因为他不能保证服务器与客户端的同步(这也是为什么UNET会在2019版本被删除的原因吧)

  • 继承了NetworkBehaviour写了个NetObjBase的类,override了OnAllGamePlayerLoaded的方法(该方法在所有玩家准备完成后通知),用于保证所有玩家准备完毕后才进行游戏逻辑判断

    [RequireComponent(typeof(NetworkIdentity))]
    public class NetObjBase : NetworkBehaviour {
    protected bool isAllGamePlayerLoaded = false;
    
    protected virtual void OnAllGamePlayerLoaded() { }
    
    protected virtual void OnUpdate() { }
    
    protected virtual void OnFixedUpdate() { }
    
    // Use this for initialization
    IEnumerator Start () {   //因为这个类重写了Start方法,所以继承这个类的子类,不能有Start方法
        while (true)
        {
            if (GamePlayer.localPlayer
                && GamePlayer.localPlayer.isAllGamePlayerLoaded)  //GamePlayer由LobbyManager提供,继承自NetworkBehaviour,其中有一个变量isAllGamePlayerLoaded是在LobbyManager加载完所有玩家后使用TargetRpc发送消息给GamePlayer设置的,用于保证所有玩家都加载完毕  //所有TargetRpc标记的函数,都要使用Target开头
            {
                OnAllGamePlayerLoaded();  //调用我们需要重载的OnAllGamePlayerLoaded,用来代替原来的Start
                isAllGamePlayerLoaded = true;
                yield break;
            }
    
            yield return null;
        }
    
    }
    
    // Update is called once per frame
    void Update () {
        if (!isAllGamePlayerLoaded)  //当所有玩家都加载完毕后才响应Update和FixedUpdate事件,需要重载OnUpdate和OnFixedUpdate,且子类不能重写Update和OnFixedUpdate
            return;
    
        OnUpdate();
    }
    
    void FixedUpdate()
    {
        if (!isAllGamePlayerLoaded)
            return;
    
        OnFixedUpdate();
    }
    }
  • 可以在一个GameMgr的节点上绑定ServerCtrl脚本设置位置:

    public class ServerCtrl : NetObjBase    //serverCtrl其实是绑定到Host主机上的Server端的,Host里负责Server的功能
    {
    protected override void OnAllGamePlayerLoaded()
    {
        Transform spawnMgr  = transform.root.Find("SpawnMgr");
        //Transform[] spawnChildPoses = spawnMgr.GetComponentsInChildren<Transform>(); //GetCompintsInChildren也会包含自己的节点,这不是我们想要的结果
    
        List<Transform> posList = new List<Transform>();
    
        foreach (Transform transform in spawnMgr)  //Transform实现了IEnmerable的接口,所以可以遍历,你们存放的是子节点的Transform组件
        {
            posList.Add(transform);  //这些Transform就是出生点
        }
    
        foreach (var kv in LobbyManager.s_Singleton._lobbyToGamePlayers) //LobbyManager在启动的时候写了DontDestroyOnLoad(gameObject);所以会保存到这个场景,里面有个Dictionary记录了Lobby Name和Player Name的对应关系,在这就是取得GamePlayer
        {
            GamePlayer gp = kv.Value;
    
            int n = Random.Range(0, posList.Count);  //不包含上界
            Transform trans = posList[n];
            gp.GetComponent<Sphere>().startPos = trans.position;
            posList.RemoveAt(n);
        }
    }
    }
  • 玩家脚本添加:

    [SyncVar(hook = "OnSyncStartPos")]  //标签就是一个类啊,后面的括号就是构造参数 表明在服务器StartPos变量改变后会发个OnSyncStartPos的消息过来 //标签有点类似于goto的感觉
    public Vector3 startPos;
    
    void OnSyncStartPos(Vector3 val)
    {
        startPos = val; //注意:当设置hook后,syncVar不会自动将val设置给StartPos,需要自己赋值
    
        this.transform.position = startPos;
        this.GetComponent<Rigidbody>().isKinematic = false;  //注意,因为我们是在所有玩家都准备好了之后,才开始涉足StartPos的,且网络同步也需要时间,如果一开始就把小球的该属性设置为false,那么他们在一开始创建时,就会互相碰撞弹开,所以一般在设置完初始位置后再开启GameObject的物理属性
    }
  • 把方块也放到服务器中生成: //因为方块也是通信双方都关注的东西,玩家只负责控制玩家能控制的东西,其他交由服务器处理

  • 在Lobby Manager中 Spawn Info存放的就是由服务器生成的GameObject; 服务器除了生成Spwan Info中的东西外,还需要生成的就是上面的Game Player ; //因为我们没有吧方块放到Spawn Info中,所以服务器不知道有怎么个东西

  • 在所有玩家准备就绪后开始生成方块:

public class PickupMgr : NetObjBase
{
    public GameObject cubePrefab;
    public float radius = 8f;

    protected override void OnAllGamePlayerLoaded()
    {
        Debug.Log(" PickupMgr :   OnAllGamePlayerLoaded");
        if (isServer)  //这是服务器端的工作
        {
            for (int i = 0; i < 360; i += 30)
            {
                var cube = Instantiate(cubePrefab);
                cube.transform.position = new Vector3(Mathf.Cos(i * Mathf.Deg2Rad) * radius, 1, Mathf.Sin(i * Mathf.Deg2Rad) * radius);
                cube.transform.SetParent(this.transform, true);

                NetworkServer.Spawn(cube);  //在服务器上注册cube, 向客户端同步cube,删除的时候在服务器上调用Destory,也会同步到所有CLient; 且要向cube添加NetworkIndentity,并且在LobbyManager中注册到Spawnable Prefabs中
            }
        }
    }

}
  • 吃立方体染色,倒计时等网络交互功能:
  • 小球端(player的角色):
public class Sphere : NetObjBase
{
    public float moveForce = 5;
    public float jumpForce = 50;

    public Text scoreText;
    public Text winText;
    public Text timeText;

    Rigidbody rigidbody;

    [TargetRpc]   //标记为TargetRpc的标签,接收服务器调用该标签的函数,函数仅支持Target命名开头(这是单个Clicent和Server通信的标签)
    public void TargetShowWinner(NetworkConnection conn, GameObject[] winners)  //其目的是显示胜利的文本
    {
        string s = "";
        for (int i = 0; i < winners.Length; i++)
        {
            s += $"{winners[i].name}胜利!\r\n";
        }

        winText.text = s;
    }

    [SyncVar(hook = "OnSyncScore")]   //SyncVar是由服务器向客户端同步的数据成员,当SyncVar标记的变量被服务器设置时,客户端可以收到该变量改变的消息  //流程是,在客户端设置Score的值,此时该值被标记后自动发给所有Client,调用hook指定的方法
    public int Score = 0;

    void OnSyncScore(int val)
    {
        if (isLocalPlayer) //只处理自己控制的小球
        {
            Score = val;

            //this.Score++;
            scoreText.text = $"Score : {Score}";
        }
    }

    [SyncVar(hook = "OnSyncStartPos")]  //当startPos被服务器改变时,会发生这个值给所有Client,Client负责处理
    public Vector3 startPos;

    void OnSyncStartPos(Vector3 val)
    {
        startPos = val; //注意:当设置hook后,syncVar不会自动将val设置给StartPos,需要自己赋值

        if (isLocalPlayer)  //只设置自己控制的player的位置,然后由NetworkTransform同步给server,然后同步给其他Client
        {
            this.transform.position = startPos;
            this.GetComponent<Rigidbody>().isKinematic = false;
        }
    }

    [SyncVar(hook = "OnSyncCountdown")]  //Countdown,服务器端设置,CLient端同步
    public float coutdown;

    void OnSyncCountdown(float val)
    {
        coutdown = val;

        if (isLocalPlayer && timeText)
        {
            timeText.text = string.Format("{0, 5:F1}", coutdown);
        }
    }

    protected override void OnAllGamePlayerLoaded()  //相当于Start()
    {
        Debug.Log(" Sphere :   OnAllGamePlayerLoaded");
        scoreText = transform.root.Find("Canvas/ScoreText").GetComponent<Text>();
        winText = transform.root.Find("Canvas/WinText").GetComponent<Text>();
        timeText = transform.root.Find("Canvas/TimeText").GetComponent<Text>();

        rigidbody = this.GetComponent<Rigidbody>();
    }

    protected override void OnFixedUpdate() //OnFixedUpdate是检测到所有玩家都加载好后,才开始调用, 相当于FixedUpdate()
    {
        if (!isLocalPlayer)
            return;

        Time.timeScale = 0.5f;      //子弹时间,加速减速
        if (Input.GetKey(KeyCode.LeftArrow))            //unity事先就检测到了所有按键,所以在这可以直接判断按键是否按下,用if判断就可以同时施加不同方向上的力   //GetKey是检测按键的持续情况,GetKeyUp和GetKeyDown是只有在按键的上、下边缘才会检测,可以做触发器,但不宜实时控制运动
        {
            rigidbody.AddForce(-moveForce * Time.fixedDeltaTime, 0, 0);
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            rigidbody.AddForce(moveForce * Time.fixedDeltaTime, 0, 0);
        }
        if (Input.GetKey(KeyCode.UpArrow))
        {
            rigidbody.AddForce(0, 0, moveForce * Time.fixedDeltaTime);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            rigidbody.AddForce(0, 0, -moveForce * Time.fixedDeltaTime);
        }
        if (Input.GetKey(KeyCode.Space))
        {
            rigidbody.AddForce(0, jumpForce * Time.fixedDeltaTime, 0);
        }

    }

    [ServerCallback] //Unity回调的函数只在服务器端执行   //ServerCallback表示只在服务器端执行,在UNET的框架下,server,所有Client都有所有一套一模一样的镜像,在这个程序中Client只负责控制角色的移动,然后通过NetworkTransform组件同步到所有其他Client和Server,Server进行统一的事件处理,然后通过广播ClientRpc或者单播TargetRpc的形式通知到各个Client更新数据
    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "pickup")
        {
            //         other.gameObject.SetActive(false);

            this.Score++;  //Score被标记为SyncVar,该值改变会同步到所有Client

            if (other.GetComponent<CubeRatate>().owner != null)
            {
                other.GetComponent<CubeRatate>().owner.GetComponent<Sphere>().Score--;  //碰到小球的玩家加分,其他减分
            }

            other.GetComponent<CubeRatate>().owner = this.gameObject;

        }
    }

}
  • 方块自己:

    public class CubeRatate : NetObjBase
    {
    [SyncVar(hook = "OnSyncOwner")]  //当cube的ower改变时,由Server广播到所有Client;
    public GameObject owner;
    
    void OnSyncOwner(GameObject val)
    {
        owner = val;
    
        this.GetComponent<MeshRenderer>().material.color = val.GetComponent<MeshRenderer>().material.color;
    }
    
    protected override void OnUpdate()
    {
        Vector3 rot = new Vector3(45, 45, 45);
        rot = rot * Time.deltaTime;
        this.transform.Rotate(rot);
    }
    }
  • Server端脚本:

    public class ServerCtrl : NetObjBase
    {
    public float countdownTotal = 60;  //游戏结束倒计时
    public float countdownCur;
    
    private bool isGameOver = false;
    
    protected override void OnAllGamePlayerLoaded()
    {
        Debug.Log("ServerCtrl :  OnAllGamePlayerLoaded");
        if (isServer) 
        {
            Transform spawnMgr = transform.root.Find("SpawnMgr");
            //Transform[] spawnChildPoses = spawnMgr.GetComponentsInChildren<Transform>(); //GetCompintsInChildren也会包含自己的节点,这不是我们想要的结果
    
            List<Transform> posList = new List<Transform>();
    
            foreach (Transform transform in spawnMgr)  //Transform实现了IEnmerable的接口,所以可以遍历,你们存放的是子节点的Transform组件
            {
                posList.Add(transform);
            }
    
            foreach (var kv in LobbyManager.s_Singleton._lobbyToGamePlayers) //LobbyManager在启动的时候写了DontDestroyOnLoad(gameObject);所以会保存到这个场景,里面有个Dictionary记录了Lobby Name和Player Name的对应关系,在这就是取得GamePlayer
            {
                GamePlayer gp = kv.Value;
    
                int n = Random.Range(0, posList.Count);  //不包含上界
                Transform trans = posList[n];
                       gp.GetComponent<Sphere>().startPos = trans.position; //所有玩家加载完毕后,由server设置所有玩家的StartPos属性,因为StartPos被标记为SyncVar,所以该属性被设置后会发给所有的Clicent,所有的Clicent负责更新自己的位置,说明Player加了NetworkTransform组件后,它的Transform就只能有自己控制(被标记为isLocalPlayer的小球),客户端要控制也只能发消息给Client自己处理
                posList.RemoveAt(n);
            }
    
            countdownCur = countdownTotal;
        }
    
    }
    
    protected override void OnUpdate()
    {
        if (isGameOver)
            return;
    
        if (isServer)  //是服务器自己
        {
            countdownCur -= Time.deltaTime;
    
            if (countdownCur < 0)
                countdownCur = 0;
    
            foreach (var kv in LobbyManager.s_Singleton._lobbyToGamePlayers)
            {
                GamePlayer gp = kv.Value;
    
                gp.GetComponent<Sphere>().coutdown = countdownCur;  //遍历所有Game Player,设置他们的coutdown, coutdown被标记为SyncVar,服务器更新后会发给所有Client;
            }
    
            if (countdownCur == 0)
            {
                isGameOver = true;
    
                int MaxScore = -1;
                List<GameObject> winners = new List<GameObject>();
                foreach (var kv in LobbyManager.s_Singleton._lobbyToGamePlayers)
                {
                    Sphere player = kv.Value.GetComponent<Sphere>();
    
                    if (player.Score > MaxScore)
                    {
                        winners.Clear();
                        winners.Add(player.gameObject);
                        MaxScore = player.Score;
                    }
                    else if (player.Score == MaxScore)
                    {
                        winners.Add(player.gameObject);
                    }
                }
    
                foreach (var kv in LobbyManager.s_Singleton._lobbyToGamePlayers)
                {
                    Sphere player = kv.Value.GetComponent<Sphere>();
    
                    player.TargetShowWinner(player.connectionToClient, winners.ToArray());  //TargetShowWinner给标记为TargetRpc,当调用该函数时,Server会通知单个Client,调用他们的TargetShowWinner方法
                }
            }
        }
    }
    }
  • //这样看来,这套解决方案,是根据预制体和NetworkMananger(LobbyManager)实时创建player和其他需要交互的GameObject,并同步所有Server和Client,然后根据Client的NetworkTransform负责位置同步,command发送消息给Server,由Server进行所有物理,逻辑处理,然后把处理结果发给所有Client,Client进行更新,然后只负责处理针对自己的更新,其他更新交由其他CLient处理,并由NetworkTransform负责同步

  • UNET的官方案例:

  • 使血条始终面向摄像机 //BillBoard

void Update {
transform.LookAt(Camera.main.transform);
}
  • NetworkManager中的Spwan Info中也包含这Player Prefab和需要Spwan出来的所有预制体的集合:
  • player Spawn Method选择Round Robin,可以使所有玩家的初生点在带有Network Start point组件的位置(如上图中的SpawanPostion0和SpwanPosition1都是只带有Network Start point和Transform的组件),也可以通过编程实现这点:
private NetworkStartPosition[] startPositions;
    private void Start()
    {
        if (!isLocalPlayer)
            return;

        startPositions = GameObject.FindObjectsOfType<NetworkStartPosition>();
    }
transform.position = startPositions[Random.Range(0, startPositions.Length)].transform.position;  
  • player的控制代码:

    public class Player : NetworkBehaviour
    {
    public GameObject bulletPrefab;
    public Transform bulletTransform;
    
    public const int maxHealth = 100;
    
    private NetworkStartPosition[] startPositions;
    
    private void Start()
    {
        if (!isLocalPlayer)
            return;
    
        startPositions = GameObject.FindObjectsOfType<NetworkStartPosition>();
    }
    
    [SyncVar(hook = "OnChangeHealth")]   //由服务器设置currentHealth后,会调用设置所有客户端的OnChangeHealth方法
    public int currentHealth = maxHealth;
    
    void OnChangeHealth(int currentHealth)
    {
        this.currentHealth = currentHealth; 
    }
    
    //[Client]  //Client 才会执行该方法
     [Server]  //Server 才会执行该方法的标签
    public void TakeDamage(int amount)
    {
        if (!isServer)
            return;
        currentHealth -= amount;  //均会触发消息给Client
    
        if (currentHealth <= 0)
        {
            currentHealth = maxHealth;
            Respawn();  //Respawn被标记为ClientRpc,当执行到这句时,只是server发送所有消息给所有CLient
        }
    }
    
    [ClientRpc]  //标记为ClientRpc,当server调用该方法,r发送消息给所有CLient
    void Respawn()
    {
        if (!isLocalPlayer)
            return;
    
        transform.position = startPositions[Random.Range(0, startPositions.Length)].transform.position;  //因为NetworkTransform的作用,client收到该消息设置positon后,又会同步给所有server和Client
    }
    
    // Update is called once per frame
    void Update()
    {
        if (!isLocalPlayer)
            return;
    
        var x = Input.GetAxis("Horizontal") * Time.deltaTime * 150.0f;
        var z = Input.GetAxis("Vertical") * Time.deltaTime * 3.0f;
    
        transform.Rotate(0, x, 0);
        transform.Translate(0, 0, z);
    
        if (Input.GetKeyDown(KeyCode.Space))
        {
            CmdFire(); //这个方法被标记被Command类型,调用时,只是发生了个消息给客户端,实际执行是在客户端发生的
        }
    }
    
     [Command]  //标记为Command标签后,调用该方法,就是发送消息给服务器
    private void CmdFire() //实际在客户端执行
    {
        GameObject bullet = Instantiate(bulletPrefab, bulletTransform.position, bulletTransform.rotation);
        bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 6;
    
        NetworkServer.Spawn(bullet); //注册cube,并同步到所有Client  //除了玩家,所有由客户端控制的GameObject都通过该方法动态同步
    
        Destroy(bullet, 2f);  //销毁本地和所有Client的bullet
    }
    }
  • 子弹脚本:

    public class Bullet : NetworkBehaviour
    {
    
    private void OnCollisionEnter(Collision collision)
    {
        if (!isServer)  //只由服务器处理所有逻辑
            return;
    
        Destroy(this.gameObject);  //这个Bullet由server生成,由Server管理,所以只有服务器中执行到这句话时才会消耗所有client的bullet
    
        Player player = collision.gameObject.GetComponent<Player>();
        player.TakeDamage(10);
    }
    }
  • 值得一提的是子弹的NetWork Transrom的同步速率为0,只有在创建的时候给个位置就不管了,这种不受玩家控制的物体不需要同步,因为所有server和Client都按照一样的轨迹运行

1 对 “Roll A Boll & UNET”的想法;

  1. Pingback: viagra online

发表评论