Unity之PUN2插件实现多人联机射击游戏

news2024/11/18 22:34:37

目录

📖一、准备工作

📺二、UI界面处理 

📱2.1 登录UI并连接PUN2服务器

📱2.2 游戏大厅界面UI

📱2.3 创建房间UI

📱2.4 进入房间UI 

📱2.5 玩家准备状态

📱2.6 加载战斗场景

📱2.7 死亡UI界面和复活按钮

🎮三、角色控制器

💣3.1 生成角色

💣3.2 角色控制和战斗系统

💣3.3 枪的脚本

⚒️四、项目打包导出 


周五的下午永远要比周六幸福,周五好啊大家有在认真摸鱼吗。前两天我突发奇想想做联机游戏,就去找教程,肝了一天终于做出来了。


做的这个实例是通过PUN2实现的,看一下效果:


先说一下搜寻资料过程中找到的实现游戏联机的方式:暂时就记录了这11个。

  1. Unity自带的UNET(Unity Networking)
  2. PUN(Photon Unity Networking)
  3. Mirror:Mirror是UNET的现代替代品
  4. 自定义网络解决方案
  5. Socket编程:系统级的API,通过调用这些API就可以实现网络通讯
  6. WebSocket:是一种在单个TCP连接上进行双工通信的协议,可用于实现多人联机游戏的数据传输和实时通信。
  7. MirrorLite:MirrorLite是Mirror的轻量级版本
  8. 树莓派及LAN连接
  9. UnityMultiplayer
  10. 自建基于TCP/IP的服务器
  11. WebRTC

个人感觉这套模型和这个教程泰裤辣,能跟着做完这个游戏Demo也是很开心的,下面依然以博客的形式记录实现这个游戏的过程。

一、准备工作

首先新建一个U3D项目导入素材包。

https://download.csdn.net/download/qq_48512649/88858525icon-default.png?t=N7T8https://download.csdn.net/download/qq_48512649/88858525去Unity官方资源商店下载PUN2插件导入到项目中

要去PUN2官网申请PUN2账号获取AppID,获取AppID教程参考下面这篇文章:不同的是Photon Type要改为PUN

PUN-注册账号以及创建应用(1)_photon 注册不了-CSDN博客文章浏览阅读898次。PUN注册账号及创建应用_photon 注册不了https://blog.csdn.net/weixin_38484443/article/details/125629797

photon pun2 设置中国区_photon中国区-CSDN博客文章浏览阅读1.9k次,点赞2次,收藏14次。pun2 中国区设置_photon中国区https://blog.csdn.net/qq_37350725/article/details/124657623?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170865675116800225534042%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170865675116800225534042&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-124657623-null-null.142%5Ev99%5Epc_search_result_base3&utm_term=PUN2%E8%AE%BE%E7%BD%AE%E6%88%90%E4%B8%AD%E5%9B%BD%E5%8C%BA&spm=1018.2226.3001.4187      哎,小编的国区申请还没回应,目前我只测试了在局域网内的联机。

生成AppID后把它复制到插件中去

粘贴生成好的AppID: 

二、UI界面处理 

2.1 登录UI并连接PUN2服务器

玩家登录场景是login,战斗场景是game

双击登录场景,编写Game脚本挂载到Game上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;

public class Game : MonoBehaviour
{
    public static UIManager uiManager;

    public static bool isLoaded = false;

    private void Awake()
    {
        if (isLoaded == true)
        {
            Destroy(gameObject);
        }
        else
        {
            isLoaded = true;
            DontDestroyOnLoad(gameObject);   //跳转场景当前游戏物体不删除
            uiManager = new UIManager();
            uiManager.Init();

            //设置发送  接收消息频率 降低延迟
            PhotonNetwork.SendRate = 50;
            PhotonNetwork.SerializationRate = 50;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //显示登录界面
        uiManager.ShowUI<LoginUI>("LoginUI");
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 LoginUI脚本,对开始游戏退出游戏按键的处理

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;

//登录界面
public class LoginUI : MonoBehaviour,IConnectionCallbacks
{
    // Start is called before the first frame update
    void Start()
    {
        transform.Find("startBtn").GetComponent<Button>().onClick.AddListener(onStartBtn);
        transform.Find("quitBtn").GetComponent<Button>().onClick.AddListener(onQuitBtn);
    }

    private void OnEnable()
    {
        PhotonNetwork.AddCallbackTarget(this);  //注册pun2事件
    }

    private void OnDisable()
    {
        PhotonNetwork.RemoveCallbackTarget(this);  //注销pun2事件
    }

    public void onStartBtn()
    {
        Game.uiManager.ShowUI<MaskUI>("MaskUI").ShowMsg("正在连接服务器...");
        
        //连接pun2服务器
        PhotonNetwork.ConnectUsingSettings();   //成功后会执行OnConnectedToMaster函数
    }

    public void onQuitBtn()
    {
        Application.Quit();
    }

    public void OnConnected()
    {
        
    }

    //连接成功后执行的函数
    public void OnConnectedToMaster()
    {
        //关闭所有界面
        Game.uiManager.CloseAllUI();
        Debug.Log("连接成功");
        //显示大厅界面
        Game.uiManager.ShowUI<LobbyUI>("LobbyUI");
    }

    //断开服务器执行的函数
    public void OnDisconnected(DisconnectCause cause)
    {
        Game.uiManager.CloseUI("MaskUI");
    }

    public void OnRegionListReceived(RegionHandler regionHandler)
    {
        
    }

    public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
    {
        
    }

    public void OnCustomAuthenticationFailed(string debugMessage)
    {
        
    }
    
}

MaskUI脚本提供遮罩界面文字显示的公共调用方法

 using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 using UnityEngine.UI;

 //遮罩界面
public class MaskUI : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    public void ShowMsg(string msg)
    {
        transform.Find("msg/bg/Text").GetComponent<Text>().text = msg;
    }
}

连接服务器成功可以看到控制台打印并输出

2.2 游戏大厅界面UI

编写脚本  LobbyUI  处理游戏大厅界面

 using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
 using UnityEngine.UI;
     
 //大厅界面
public class LobbyUI : MonoBehaviourPunCallbacks
{
    private TypedLobby lobby;  //大厅对象

    private Transform contentTf;
    private GameObject roomPrefab;
    void Start()
    {
        //关闭按钮
        transform.Find("content/title/closeBtn").GetComponent<Button>().onClick.AddListener(onCloseBtn);
        //创建房间按钮
        transform.Find("content/createBtn").GetComponent<Button>().onClick.AddListener(onCreateRoomBtn);
        //刷新按钮
        transform.Find("content/updateBtn").GetComponent<Button>().onClick.AddListener(onUpdateRoomBtn);

        contentTf = transform.Find("content/Scroll View/Viewport/Content");
        roomPrefab = transform.Find("content/Scroll View/Viewport/item").gameObject;
        
        lobby = new TypedLobby("fpsLobby", LobbyType.SqlLobby); //1.大厅名字  2.大厅类型(可搜索)
        //进入大厅
        PhotonNetwork.JoinLobby(lobby);
    }
    
    //进入大厅回调
    public override void OnJoinedLobby()
    { 
        Debug.Log("进入大厅...");
    }

    //创建房间
    public void onCreateRoomBtn()
    {
        Game.uiManager.ShowUI<CreateRoomUI>("CreateRoomUI");
    }

    //关闭大厅界面
    public void onCloseBtn()
    {
        //断开连接
        PhotonNetwork.Disconnect();
        Game.uiManager.CloseUI(gameObject.name);
        //显示登录界面
        Game.uiManager.ShowUI<LoginUI>("LoginUI");
    }
    
    //刷新房间列表
    public void onUpdateRoomBtn()
    {
        Game.uiManager.ShowUI<MaskUI>("MaskUI").ShowMsg("刷新中...");
        PhotonNetwork.GetCustomRoomList(lobby, "1");   //执行该方法后会触发OnRoomListUpdate回调
    }
    
    //清除已经存在的房间物体
    private void ClearRoomList()
    {
        while (contentTf.childCount != 0)
        {
            DestroyImmediate(contentTf.GetChild(0).gameObject);
        }
    }

    //刷新房间后的回调
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Game.uiManager.CloseUI("MaskUI");
        Debug.Log("房间刷新");

        ClearRoomList();
        for (int i = 0; i < roomList.Count; i++)
        {
            GameObject obj = Instantiate(roomPrefab, contentTf);
            obj.SetActive(true);
            string roomName = roomList[i].Name;  //房间名称
            obj.transform.Find("roomName").GetComponent<Text>().text = roomName;
            obj.transform.Find("joinBtn").GetComponent<Button>().onClick.AddListener(delegate()
            {
                Debug.Log(roomName);
                //加入房间
                Game.uiManager.ShowUI<MaskUI>("MaskUI").ShowMsg("加入中...");

                PhotonNetwork.JoinRoom(roomName); //加入房间
            });
        }
    }

    public override void OnJoinedRoom()
    {
        //加入房间回调
        Game.uiManager.CloseAllUI();
        Game.uiManager.ShowUI<RoomUI>("RoomUI");
    }

    public override void OnJoinRoomFailed(short returnCode, string message)
    {
        //加入房间失败
        Game.uiManager.CloseUI("MaskUI");
    }
}

2.3 创建房间UI

创建房间脚本 CreateRoomUI

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;

public class CreateRoomUI : MonoBehaviourPunCallbacks
{
    private InputField roomNameInput;  //房间名称
    void Start()
    {
        transform.Find("bg/title/closeBtn").GetComponent<Button>().onClick.AddListener(onCloseBtn);
        transform.Find("bg/okBtn").GetComponent<Button>().onClick.AddListener(onCreateBtn);
        roomNameInput = transform.Find("bg/InputField").GetComponent<InputField>();
        
        //随机一个房间名称
        roomNameInput.text = "room_" + Random.Range(1, 9999); 
    }

    //创建房间
    public void onCreateBtn()
    {
        Game.uiManager.ShowUI<MaskUI>("MaskUI").ShowMsg("创建中...");
        RoomOptions room = new RoomOptions();
        room.MaxPlayers = 8;  //房间最大玩家数
        PhotonNetwork.CreateRoom(roomNameInput.text, room);  //1.房间名称 2.房间的对象参数
    }

    //关闭按钮
    public void onCloseBtn()
    {
        Game.uiManager.CloseUI(gameObject.name);
    }
    
    //创建成功后回调
    public override void OnCreatedRoom()
    {
        Debug.Log("创建成功");
        Game.uiManager.CloseAllUI();
        //显示房间UI
        Game.uiManager.ShowUI<RoomUI>("RoomUI");
    }
    
    //创建失败
    public override void OnCreateRoomFailed(short returnCode, string message)
    {
        Game.uiManager.CloseUI("MaskUI");
    }
}

2.4 进入房间UI 

创建房间完成后会进入到房间里  编写RoomUI脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;

public class RoomUI : MonoBehaviour,IInRoomCallbacks
{
    Transform startTf; 
    Transform contentTf;
    GameObject roomPrefab;
    public List<RoomItem> roomList;
    private void Awake()
    {
        roomList = new List<RoomItem>();
        contentTf = transform.Find("bg/Content");
        roomPrefab = transform.Find("bg/roomItem").gameObject;
        transform.Find("bg/title/closeBtn").GetComponent<Button>().onClick.AddListener(onCloseBtn);
        startTf = transform.Find("bg/startBtn");
        startTf.GetComponent<Button>().onClick.AddListener(onStartBtn);

        PhotonNetwork.AutomaticallySyncScene = true; //执行PhotonNetwork.LoadLevel加载场景的时候 其他玩家也跳转相同的场景
    }

    void Start()
    {
        //生成房间里的玩家项
        for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
        {
            Player p = PhotonNetwork.PlayerList[i];
            CreateRoomItem(p);
        }
    }

    private void OnEnable()
    {
        PhotonNetwork.AddCallbackTarget(this);
    }

    private void OnDisable()
    {
        PhotonNetwork.RemoveCallbackTarget(this);
    }

    //生成玩家
    public void CreateRoomItem(Player p)
    {
        GameObject obj = Instantiate(roomPrefab, contentTf);
        obj.SetActive(true);
        RoomItem item = obj.AddComponent<RoomItem>();
        item.owerId = p.ActorNumber;  //玩家编号
        roomList.Add(item);

        object val;
        if (p.CustomProperties.TryGetValue("IsReady", out val))
        {
            item.IsReady = (bool)val;
        }
    }

    //删除离开房间的玩家
    public void DeleteRoomItem(Player p)
    {
        RoomItem item = roomList.Find((RoomItem _item) => { return p.ActorNumber == _item.owerId; });
        if (item != null)
        {
            Destroy(item.gameObject);
            roomList.Remove(item);
        }
    }

    //关闭
    void onCloseBtn()
    {
        //断开连接
        PhotonNetwork.Disconnect();
        Game.uiManager.CloseUI(gameObject.name);
        Game.uiManager.ShowUI<LoginUI>("LoginUI");
    }
    
    //开始游戏
    void onStartBtn()
    {
         //加载场景 让房间里的玩家也加载场景
         PhotonNetwork.LoadLevel("game");
    }

    //新玩家进入房间
    public void OnPlayerEnteredRoom(Player newPlayer)
    {
        CreateRoomItem(newPlayer);
    }

    //房间里的其他玩家离开房间
    public void OnPlayerLeftRoom(Player otherPlayer)
    {
        DeleteRoomItem(otherPlayer);
    }

    public void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged)
    {
        
    }

    //玩家自定义参数更新回调
    public void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps)
    {
        RoomItem item = roomList.Find((_item) => { return _item.owerId == targetPlayer.ActorNumber; });
        if (item != null)
        {
            item.IsReady = (bool)changedProps["IsReady"];
            item.ChangeReady(item.IsReady);
        }
        
        //如果是主机玩家判断所有玩家的准备状态
        if (PhotonNetwork.IsMasterClient)
        {
            bool isAllReady = true;
            for (int i = 0; i < roomList.Count; i++)
            {
                if (roomList[i].IsReady == false)
                {
                    isAllReady = false;
                    break;
                }
            }
            startTf.gameObject.SetActive(isAllReady); //开始按钮是否显示
        }
    }

    public void OnMasterClientSwitched(Player newMasterClient)
    {
        
    }
}

2.5 玩家准备状态

玩家进入房间后会显示信息和准备状况,编写RoomItem脚本实现。只有房间内所有玩家都处于准备状态房主才能开始游戏。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;

public class RoomItem : MonoBehaviour
{

    public int owerId;  //玩家编号
    public bool IsReady = false; //是否准备
    void Start()
    {
        if (owerId == PhotonNetwork.LocalPlayer.ActorNumber)
        {
            transform.Find("Button").GetComponent<Button>().onClick.AddListener(OnReadyBtn);
        }
        else
        {
            transform.Find("Button").GetComponent<Image>().color = Color.black;
        }

        ChangeReady(IsReady);
    }

    public void OnReadyBtn()
    {
        IsReady = !IsReady;

        ExitGames.Client.Photon.Hashtable table = new ExitGames.Client.Photon.Hashtable();

        table.Add("IsReady", IsReady);

        PhotonNetwork.LocalPlayer.SetCustomProperties(table); //设置自定义参数

        ChangeReady(IsReady);
    }

    public void ChangeReady(bool isReady)
    {
        transform.Find("Button/Text").GetComponent<Text>().text = isReady == true ? "已准备" : "未准备";
    }
}

 2.6 加载战斗场景

双击切换到战斗场景game中,编写FightManager脚本挂载到fight

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;

public class FightManager : MonoBehaviour
{
    private void Awake()
    {
        //隐藏鼠标
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        
        //关闭所有界面
        Game.uiManager.CloseAllUI();
        //显示战斗界面
        Game.uiManager.ShowUI<FightUI>("FightUI");

        Transform pointTf = GameObject.Find("Point").transform;

        Vector3 pos = pointTf.GetChild(Random.Range(0, pointTf.childCount)).position;
        
        //实例化角色
        PhotonNetwork.Instantiate("Player", pos, Quaternion.identity);  //实例化的资源要放在Resources文件夹
    }
}

编写战斗场景UI界面处理脚本FightUI

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class FightUI : MonoBehaviour
{
    private Image bloodImg;
    void Start()
    {
        bloodImg = transform.Find("blood").GetComponent<Image>();
    }
    
    //更新子弹个数显示
    public void UpdateBulletCount(int count)
    {
        transform.Find("bullet/Text").GetComponent<Text>().text = count.ToString();
    }

    //更新血量
    public void UpdateHp(float cur, float max)
    {
        transform.Find("hp/fill").GetComponent<Image>().fillAmount = cur / max;
        transform.Find("hp/Text").GetComponent<Text>().text = cur + "/" + max;
    }

    public void UpdateBlood()
    {
        StopAllCoroutines();
        StartCoroutine(UpdateBloodCo());
    }

    public IEnumerator UpdateBloodCo()
    {
        bloodImg.color = Color.white;
        Color color = bloodImg.color;
        float t = 0.35f;
        while (t >= 0)
        {
            t -= Time.deltaTime;
            color.a = Mathf.Abs(Mathf.Sin(Time.realtimeSinceStartup));
            bloodImg.color = color;

            yield return null;
        }

        color.a = 0;
        bloodImg.color = color;
    }
}

2.7 死亡UI界面和复活按钮

编写脚本 LossUI

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class LossUI : MonoBehaviour
{
    public System.Action onClickCallBack;
    // Start is called before the first frame update
    void Start()
    {
        transform.Find("resetBtn").GetComponent<Button>().onClick.AddListener(OnClickBtn);
    }

    public void OnClickBtn()
    {
        if (onClickCallBack != null)
        {
            onClickCallBack();
        }

        Game.uiManager.CloseUI(gameObject.name);
    }
}

三、角色控制器

3.1 生成角色

给角色Player挂载Photon View组件,实例化生成玩家的代码我们已经在FightManager脚本中实现了。

3.2 角色控制和战斗系统

编写角色控制脚本PlayerController挂载到角色上,战斗系统的逻辑也在这个脚本里,其中的参数数值可以参考下图:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

//角色控制器 
public class PlayerController : MonoBehaviourPun,IPunObservable
{

    //组件
    public Animator ani;
    public Rigidbody body;
    public Transform camTf;  //跟随的相机
    
    //数值
    public int CurHp = 10;
    public int MaxHp = 10;
    public float MoveSpeed = 5f;

    public float H; //水平值
    public float V; //垂直值
    public Vector3 dir; //移动方向

    public Vector3 offset; //摄像机与角色之间的偏移值

    public float Mouse_X; //鼠标偏移值
    public float Mouse_Y;
    public float scroll; //鼠标滚轮值
    public float Angle_X; //x轴的旋转角度
    public float Angle_Y; //y轴的旋转角度

    public Quaternion camRotation; //摄像机旋转的四元数

    public Gun gun; //枪的脚本
    
    //声音
    public AudioClip reloadClip;
    public AudioClip shootClip;

    public bool isDie = false;

    public Vector3 currentPos;
    public Quaternion currentRotation;
    
    void Start()
    {
        Angle_X = transform.eulerAngles.x;
        Angle_Y = transform.eulerAngles.y;
        
        ani = GetComponent<Animator>();
        body = GetComponent<Rigidbody>();
        gun = GetComponentInChildren<Gun>();
        camTf = Camera.main.transform;
        currentPos = transform.position;
        currentRotation = transform.rotation;
        if (photonView.IsMine)
        {
            Game.uiManager.GetUI<FightUI>("FightUI").UpdateHp(CurHp, MaxHp);
        }
    }


    void Update()
    {
        //判断是否是本机玩家  只能操作本机角色
        if (photonView.IsMine)
        {
            if (isDie == true)
            {
                return;
            }
            UpdatePosition();
            UpdateRotation();
            InputCtl();
        }
        else
        {
            UpdateLogic();
        }
    }
    
    //其他角色更新发送过来的数据(位置 旋转)
    public void UpdateLogic()
    {
        transform.position = Vector3.Lerp(transform.position, currentPos, Time.deltaTime * MoveSpeed * 10);
        transform.rotation = Quaternion.Slerp(transform.rotation, currentRotation, Time.deltaTime * 500);
    }

    private void LateUpdate()
    {
        ani.SetFloat("Horizontal", H);
        ani.SetFloat("Vertical", V);
        ani.SetBool("isDie", isDie);
    }

    //更新位置
    public void UpdatePosition()
    {
        H = Input.GetAxisRaw("Horizontal");
        V = Input.GetAxisRaw("Vertical");
        dir = camTf.forward * V + camTf.right * H;
        body.MovePosition(transform.position + dir * Time.deltaTime * MoveSpeed);
    }
    
    //更新旋转(同时设置摄像机的位置的旋转值)
    public void UpdateRotation()
    {
        Mouse_X = Input.GetAxisRaw("Mouse X");
        Mouse_Y = Input.GetAxisRaw("Mouse Y");
        scroll = Input.GetAxis("Mouse ScrollWheel");

        Angle_X = Angle_X - Mouse_Y;
        Angle_Y = Angle_Y + Mouse_X;

        Angle_X = ClampAngle(Angle_X, -60, 60);
        Angle_Y = ClampAngle(Angle_Y, -360, 360);

        camRotation = Quaternion.Euler(Angle_X, Angle_Y, 0);

        camTf.rotation = camRotation;

        offset.z += scroll;
        
        camTf.position = transform.position + camTf.rotation * offset;

        transform.eulerAngles = new Vector3(0, camTf.eulerAngles.y, 0);
    }

    //角色操作
    public void InputCtl()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //判断子弹个数
            if (gun.BulletCount > 0)
            {
                //如果正在播放填充子弹的动作不能开枪
                if (ani.GetCurrentAnimatorStateInfo(1).IsName("Reload"))
                {
                    return;
                }

                gun.BulletCount--;
                Game.uiManager.GetUI<FightUI>("FightUI").UpdateBulletCount(gun.BulletCount);
                //播放开火动画
                ani.Play("Fire", 1, 0);

                StopAllCoroutines();
                StartCoroutine(AttackCo());
            }
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            //填充子弹
            AudioSource.PlayClipAtPoint(reloadClip, transform.position); //播放填充子弹的声音
            ani.Play("Reload");
            gun.BulletCount = 10;
            Game.uiManager.GetUI<FightUI>("FightUI").UpdateBulletCount(gun.BulletCount);
        }
    }
    
    //攻击协同程序
    IEnumerator AttackCo()
    {
        //延迟0.1秒才发射子弹
        yield return new WaitForSeconds(0.1f);
        //播放射击音效
        AudioSource.PlayClipAtPoint(shootClip, transform.position);
        
        //射线检测 鼠标中心点发送射线
        Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width * 0.5f, Screen.height * 0.5f,Input.mousePosition.z));
        //射线可以改成在枪口位置为起始点 发送,避免射线射到自身

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 10000, LayerMask.GetMask("Player")))
        {
            Debug.Log("射到角色");
            hit.transform.GetComponent<PlayerController>().GetHit();
        }

        photonView.RPC("AttackRpc", RpcTarget.All);  //所有玩家执行 AttackRpc 函数
    }

    [PunRPC]
    public void AttackRpc()
    {
        gun.Attack();
    }

    //受伤
    public void GetHit()
    {
        if (isDie == true)
        {
            return;
        }
        
        //同步所有角色受伤
        photonView.RPC("GetHitRPC", RpcTarget.All);
    }

    [PunRPC]
    public void GetHitRPC()
    {
        CurHp -= 1;  //扣一滴血
        if (CurHp <= 0)
        {
            CurHp = 0;
            isDie = true;
        }

        if (photonView.IsMine)
        {
            Game.uiManager.GetUI<FightUI>("FightUI").UpdateHp(CurHp, MaxHp);
            Game.uiManager.GetUI<FightUI>("FightUI").UpdateBlood();
            
            if (CurHp == 0)
            {
                Invoke("gameOver", 3);  //3秒后显示失败界面       
            }
        }
    }

    private void gameOver()
    {
        //显示鼠标
        Cursor.visible = true;
        Cursor.lockState = CursorLockMode.None;
        //显示失败界面
        Game.uiManager.ShowUI<LossUI>("LossUI").onClickCallBack = OnReset;
    }

    //复活
    public void OnReset()
    {
        //隐藏鼠标
        Cursor.visible = false;
        Cursor.lockState = CursorLockMode.Locked;
        photonView.RPC("OnResetRPC", RpcTarget.All);
    }

    [PunRPC]
    public void OnResetRPC()
    {
        isDie = false;
        CurHp = MaxHp;
        if (photonView.IsMine)
        {
            Game.uiManager.GetUI<FightUI>("FightUI").UpdateHp(CurHp, MaxHp);
        }
    }

    //限制角度在-360 到 360之间
    public float ClampAngle(float val, float min, float max)
    {
        if (val > 360)
        {
            val -= 360;
        }

        if (val < -360)
        {
            val += 360;
        }

        return Mathf.Clamp(val, min, max);
    }

    private void OnAnimatorIK(int layerIndex)
    {
        if (ani != null)
        {
            Vector3 angle = ani.GetBoneTransform(HumanBodyBones.Chest).localEulerAngles;
            angle.x = Angle_X;
            ani.SetBoneLocalRotation(HumanBodyBones.Chest, Quaternion.Euler(angle));
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            //发送数据
            stream.SendNext(H);
            stream.SendNext(V);
            stream.SendNext(Angle_X);
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
        }
        else
        {
            //接收数据
            H = (float)stream.ReceiveNext();
            V = (float)stream.ReceiveNext();
            Angle_X = (float)stream.ReceiveNext();
            currentPos = (Vector3)stream.ReceiveNext();
            currentRotation = (Quaternion)stream.ReceiveNext();
        }
    }
}

3.3 枪的脚本

编写枪的脚本Gun,挂载到Assault_Rifle_02上,并按下图把参数填充好

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//枪的脚本
public class Gun : MonoBehaviour
{

    public int BulletCount = 10;

    public GameObject bulletPrefab;
    public GameObject casingPreafab;

    public Transform bulletTf;
    public Transform casingTf;
    
    void Start()
    {
        
    }

    public void Attack()
    {
        GameObject bulletObj = Instantiate(bulletPrefab);
        bulletObj.transform.position = bulletTf.transform.position;
        bulletObj.GetComponent<Rigidbody>().AddForce(transform.forward * 500, ForceMode.Impulse);  //子弹速度   让中心点跟枪口位置可自行调整摄像机的偏移值

        GameObject casingObj = Instantiate(casingPreafab);
        casingObj.transform.position = casingTf.transform.position;
    }
}

四、项目打包导出 

  1. 文件 ——》 生成设置 

  2. 点击生成选择文件夹打包

  3. 打包好后也可以发给自己的小伙伴,双击直接可以运行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1466791.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud-Gateway解决跨域问题

Spring Cloud Gateway是一个基于Spring Framework的微服务网关&#xff0c;用于构建可扩展的分布式系统。在处理跨域问题时&#xff0c;可以通过配置网关来实现跨域资源共享&#xff08;CORS&#xff09;。要解决跨域问题&#xff0c;首先需要在网关的配置文件中添加相关的跨域…

【EI会议征稿通知】2024年软件自动化与程序分析国际学术会议(SAPA 2024)

2024年软件自动化与程序分析国际学术会议&#xff08;SAPA 2024) 2024 International Conference on Software Automation and Program Analysis 在当今科技社会中&#xff0c;软件产业呈快速发展趋势&#xff0c;软件自动化与程序分析技术在提高软件质量、降低开发成本、提升…

【Java】继承与抽象(实验三)

目录 一、实验目的 二、实验内容 三、实验小结 一、实验目的 了解继承的概念&#xff0c;掌握派生类的定义。掌握派生类构造方法的执行过程。掌握方法的重载与覆盖。掌握抽象类的概念及上转型对象的使用 二、实验内容 1、定义一个抽象类Shape&#xff0c;类中封装属性name…

详解AP3216C(三合一sensor: 光照、距离、照射强度)驱动开发

目录 概述 1 认识AP3216C 1.1 AP3216C特性 1.2 AP3216C内部结构 1.3 AP3216C 硬件电路 1.4 AP3216C工作时序 1.4.1 I2C 写数据协议 1.4.2 I2C 读数据协议 1.5 重要的寄存器 1.5.1 系统配置寄存器 1.5.2 和中断相关寄存器 1.5.3 IR数据寄存器 1.5.4 ALS 数据寄存器 …

C++之stack与queue的模拟实现

一、 stack的介绍和使用 1. stack的介绍 stack 的文档介绍 翻译&#xff1a; 1. stack 是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。 2. stack 是作为容器适配器被实现的&#xff…

C++的queue容器->基本概念、常用接口

#include<iostream> using namespace std; #include <queue> #include <string> //队列 queue class Person { public: Person(string name, int age) { this->m_Name name; this->m_Age age; } string m_Name; int…

.netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项

1、SqlSugarCore 相关 1.1 主项目添加数据&#xff0c;否则会报数据库连接错误&#xff1a; <InvariantGlobalization>false</InvariantGlobalization> <PropertyGroup><TargetFramework>net8.0</TargetFramework><Nullable>enable</…

PostMan使用自带js库base64编码、sha256摘要、环境变量的使用

目录 1、环境变量的使用2、base64编码、sha256摘要、以及脚本的使用3、脚本代码 在请求调试接口的过程中&#xff0c;因为要使用大量相同的参数&#xff0c;使用变量的方式能很大程度上减轻接口调用的工作量 版本说明&#xff1a;Postman for Windows&#xff0c;Version&#…

【办公类-16-10-02】“2023下学期 6个中班 自主游戏观察记录(python 排班表系列)

背景需求&#xff1a; 已经制作了本学期的中4班自主游戏观察记录表 【办公类-16-10-01】“2023下学期 中4班 自主游戏观察记录&#xff08;python 排班表系列&#xff09;-CSDN博客文章浏览阅读398次&#xff0c;点赞10次&#xff0c;收藏3次。【办公类-16-10-01】“2023下学…

无人机竞赛常用目标检测方法--色块检测

本次开源计划主要针对大学生无人机相关竞赛的视觉算法开发。 开源代码仓库链接&#xff1a;https://github.com/zzhmx/Using-color-gamut-limitations-such-as-HSV-and-RGB-for-object-detection.git 主要使用传统算法&#xff0c;如果想要使用进阶版机器学习算法&#xff0c;请…

golang tun设备创建并监听

golang tun设备创建并监听 linux tun设备文件地址为/dev/net/tun.直接打开即可(关闭文件描述符创建的tun虚拟接口自动注销) fd,err:syscall.Open("/dev/net/tun",syscall.O_RDWR,0640)//关闭 syscall.Close(fd)初始化 配置ip地址启动虚拟网卡 ip addr add xxx.xx…

【数据分享】中国首套1公里高分辨率大气湿度指数数据集(6个指标\免费获取)

湿度数据是气象学和许多其他领域中至关重要的数据&#xff0c;可用于气象预测与气候研究。之前我们分享过Excel格式和GIS矢量格式&#xff08;均可查看之前的文章获悉详情&#xff09;的2000-2020年全国各城市逐日、逐月和逐年的湿度数据。 本次我们给大家带来的是中国首套1公…

【Linux网络】网络编程套接字(TCP)

目录 地址转换函数 字符串IP转整数IP 整数IP转字符串IP 关于inet_ntoa 简单的单执行流TCP网络程序 TCP socket API 详解及封装TCP socket 服务端创建套接字 服务端绑定 服务端监听 服务端获取连接 服务端处理请求 客户端创建套接字 客户端连接服务器 客户端…

2023年的AI模型学习/部署/优化

可以的话&#xff0c;github上给点一个小心心&#xff0c;感谢观看。 LDC边缘检测的轻量级密集卷积神经网络&#xff1a; meiqisheng/LDC (github.com)https://github.com/meiqisheng/LDC segment-anything分割一切的图像分割算法模型&#xff1a; meiqisheng/segment-anyt…

vue2+element医院安全(不良)事件报告管理系统源代码

目录 安全不良事件类型 源码技术栈 医院安全&#xff08;不良&#xff09;事件报告管理系统采用无责的、自愿的填报不良事件方式&#xff0c;有效地减轻医护人员的思想压力&#xff0c;实现以事件为主要对象&#xff0c;可以自动、及时、实际地反应医院的安全、不良、近失事件…

flink state原理,TTL,状态后端,数据倾斜一文全

flink state原理 1. 状态、状态后端、Checkpoint 三者之间的区别及关系&#xff1f;2 算子状态与键控状态的区别2.1 算子状态2.2 键控状态2.3 算子状态api2.4 键控状态api 3 HashMapStateBackend 状态后端4 EmBeddedRocksDbStateBackend 状态后端5 状态数据结构介绍5.1 算子状态…

129 Linux 系统编程8 IO操作 系统函数 open

系统调用 什么是系统调用&#xff1a; 由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface&#xff0c;API)。是应用程序同系统之间数据交互的桥梁。 C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。 回忆我们前面学过的C标准…

ArcgisForJS如何使用ArcGIS Server发布的切片地图服务?

文章目录 0.引言1.准备海量地理数据2.ArcGIS Server发布切片地图服务3.ArcgisForJS使用ArcGIS Server发布的切片地图服务 0.引言 ArcGIS Server是一个由Esri开发的地理信息系统&#xff08;GIS&#xff09;服务器软件&#xff0c;它提供了许多功能&#xff0c;包括发布切片地图…

MIT-6.824-Lab2,Raft部分笔记|Use Go

文章目录 前记Paper6&#xff1a;RaftLEC5、6&#xff1a;RaftLAB22AtaskHintlockingstructureguide设计与编码 2BtaskHint设计与编码 2CtaskHint question后记 LEC5&#xff1a;GO, Threads, and Raftgo threads技巧raft实验易错点debug技巧 前记 趁着研一考完期末有点点空余…

springboot751社区维修平台

springboot751社区维修平台 获取源码——》公主号&#xff1a;计算机专业毕设大全