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

目录

📖一、准备工作

📺二、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 updatevoid Start(){//显示登录界面uiManager.ShowUI<LoginUI>("LoginUI");}// Update is called once per framevoid 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 updatevoid 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 updatevoid 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();//显示房间UIGame.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 updatevoid 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.rhkb.cn/news/263217.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI时代显卡如何选择,B100、H200、L40S、A100、H100、V100 含架构技术和性能对比

AI时代显卡如何选择&#xff0c;B100、H200、L40S、A100、H100、V100 含架构技术和性能对比。 英伟达系列显卡大解析B100、H200、L40S、A100、A800、H100、H800、V100如何选择&#xff0c;含架构技术和性能对比带你解决疑惑。 近期&#xff0c;AIGC领域呈现出一片繁荣景象&a…

ESP8266智能家居(4)——开发APP基础篇

1.前期准备 安装好Android studio 开发环境 准备一台完好的安卓手机 手机要处于开发者模式 设置 --->关于手机---> 一直点击版本号 &#xff08;不同手机进入开发者模式的步骤可能不太一样&#xff09; 进入开发者模式后&#xff0c;找到辅助功能&#xff0c;打开开…

字符函数和字符串函数(C语言进阶)(一)

前言 C语言中对字符和字符串的处理是很频繁的&#xff0c;但是c语言本身是没有字符串类型的&#xff0c;字符串通常放在常量字符串中或着字符数组中。 字符串常量适用于哪些对它不做修改的字符串函数。 1、函数介绍 1.1 strlen strlen&#xff1a;计算字符串长度 看一个代码&…

​LeetCode解法汇总2583. 二叉树中的第 K 大层和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一棵二叉树的根节点 root 和一个正整…

编译GreatSQL with RocksDB引擎

GreatSQL里也能用上RocksDB引擎 1. 前言 RocksDB 是基于Facebook 开源的一种支持事务的、高度可压缩、高性能的MyRocks存储引擎&#xff0c;特别适用于高度压缩和大容量的数据。以下是一些关键特点&#xff1a; 高性能&#xff1a; LSM 树结构使得RocksDB在写入密集型负载下表现…

Qt 事件

1. 事件 事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。在Qt中使用一个对象来表示一个事件&#xff0c;它继承自QEvent类。 2. 事件和信号 事件与信号并不相同&#xff0c;比如我们使用鼠标点击了一下界面上的按钮&#xff0c;那么就会产生…

微服务三十五关

1.微服务有什么好处&#xff1f; 微服务优点很多&#xff0c;但是我们通常说一个东西好肯定会跟另一个东西比较&#xff0c; 通常说微服务好会和单体项目进行比较。以下是微服务相对于单体项目的一些显著好处&#xff1a; 首先&#xff0c;让我们讨论单体项目的一些主要缺点&a…

uniapp腾讯地图JavaScript Api,H5端和原生APP端可用

因项目需要&#xff0c;在uniapp中集成使用腾讯地图&#xff0c;为了方便维护&#xff0c;希望通过一套代码实现H5和APP同时可用。H5显示相对简单&#xff0c;APP端比较麻烦&#xff0c;记录下实现过程 一、集成步骤 1.使用 renderjs script标签使用renderjs&#xff0c;因为…

Java基础之lambda表达式(五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

C# If与Switch的区别

在 switch 语句中使用表达式比较时&#xff0c;编译器会生成一个查找表&#xff0c;其中包含所有表达式的值和对应的 case 标签。因此&#xff0c;与使用常量或字面量比较相比&#xff0c;使用表达式比较可能会略微降低性能。 只有当 switch 语句中的所有 case 标签都使用常量或…

Java编程实战:构建医疗信息管理新平台

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【C++私房菜】面向对象中的多态

文章目录 一、多态二、对象的静态类型和动态类型三、虚函数和纯虚函数1、虚函数2、虚析构函数3、抽象基类和纯虚函数4、多态的原理 四、重载、覆盖(重写)、隐藏(重定义)的对比 一、多态 OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语&#xff0c;其含义是“多…

基于MPPT最大功率跟踪算法的涡轮机控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于MPPT最大功率跟踪算法的涡轮机控制系统simulink建模与仿真.mppt采用爬山法实现&#xff0c;仿真输出MPPT控制效果&#xff0c;功率&#xff0c;转速等。 2.系统仿真结果 …

ELK入门(四)-logstash

Logstash Logstash 是开源的服务器端数据处理管道&#xff0c;能够同时从多个来源采集数据&#xff0c;转换数据&#xff0c;然后将数据发送到您最喜欢的存储库中。 Logstash 能够动态地采集、转换和传输数据&#xff0c;不受格式或复杂度的影响。利用 Grok 从非结构化数据中…

电脑开机蓝屏错误代码c000021a怎么办 电脑蓝屏报错c000021a的解决办法

很多小伙伴在电脑开机的时候出现蓝屏代码c000021a都不知道该怎么去解决&#xff0c;所以今天就给你们带来了c000021a蓝屏解救方法&#xff0c;如果你还没解决的话就快来看看吧。 解决办法&#xff1a; 原因&#xff1a; c000021a蓝屏的原因有很多&#xff0c;主要有以下几种…

常用的函数式接口(Supplier、Consumer、Predicate、Function)

目录 一.函数式接口作为方法的参数 二.函数式接口作为方法的返回值 三.常用的函数式接口 3.1生产型Supplier接口 3.2消费型Consumer接口 抽象方法&#xff1a;accept 默认方法&#xff1a;andThen 3.3判断型Predicate接口 抽象方法&#xff1a;test 默认方法&#xf…

从上燃动力到未势能源,氢能领风者不再蛰伏

“世间本来没有路&#xff0c;人们沿着这条道走多了就变成了路。” 2021年&#xff0c;长城控股集团未势能源科技有限公司总裁陈雪松在一次论坛上&#xff0c;用这句话来形容氢能的发展。冥冥之中&#xff0c;未势能源的命运也像这句话描述的那样&#xff0c;从荒原中走出了一…

报错:org.springframework.jdbc.BadSqlGrammarException:

//报错 2024-02-24 19:44:10.814 ERROR 6184 --- [nio-9090-exec-5] c.e.exception.GlobalExceptionHandler : 异常信息&#xff1a; org.springframework.jdbc.BadSqlGrammarException: GPT&#xff1a; 根据异常信息&#xff0c;这是一个Spring框架抛出的BadSqlGrammar…

[算法沉淀记录] 排序算法 —— 归并排序

排序算法 —— 归并排序 算法介绍 归并排序是一种分治算法&#xff0c;由约翰冯诺伊曼在1945年发明。它的工作原理是将未排序的列表划分为n个子列表&#xff0c;每个子列表包含一个元素(包含一个元素的列表被认为是有序的)&#xff0c;然后重复合并子列表以生成新的有序子列表…

【深入理解设计模式】原型设计模式

原型设计模式 原型设计模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;它允许通过复制已有对象来创建新对象&#xff0c;而无需直接依赖它们的具体类。这种模式通常用于需要频繁创建相似对象的场景&#xff0c;以避免昂贵的创建操作或初始化过…