目录
????一、 跳跃,加速跑
????二、玩家自定义输入昵称
????2.1 给昵称赋值
????2.2 实现
????三、玩家昵称同步到房间列表
????3.1 获取全部玩家
????3.2 自定义Player中的字段
????3.3 实现
????四、计分板功能的实现
????4.1 设置玩家分数
????4.2 实现
前几天对之前肝出的射击游戏Demo进行了小小的优化,顺便在了解一下PUN插件。怎么实现的这个Demo可以来看一下这篇文章:
Unity之PUN2插件实现多人联机射击游戏-CSDN博客文章浏览阅读1.1k次,点赞19次,收藏19次。周五的下午永远要比周六幸福,周五好啊大家有在认真摸鱼吗。前两天我突发奇想想做联机游戏,就去找教程,肝了一天终于做出来了。先说一下搜寻资料过程中找到的实现游戏联机暂时就记录了这11个,做的这个实例是通过PUN2实现的,先看一下效果:个人感觉这套模型和这个教程泰裤辣,能跟着做完这个游戏Demo也是很开心的,下面依然以博客的形式记录实现这个游戏的过程。https://blog.csdn.net/qq_48512649/article/details/136249522来看一下优化完的效果。
关于优化了哪几个小点:
- 点击开始游戏玩家可以输入自己的昵称;进入到房间后玩家对应的昵称也会同步显示到房间列表上;
- 和朋友一起玩的时候他说会卡进房间的模型里建议我加上跳跃功能,我就给加上了,顺便加了一个按住Shift和方向键进行加速跑;
- 同时按住Tab键会显示出计分板,这个计分板是按照射击命中次数来计分的。
下面来记录一下这几点优化是怎么实现的
一、 跳跃,加速跑
相信对于Unity入门的人来说这两点太简单了,废话不多说直接上代码。在PlayerController这个脚本中
public float MoveSpeed = 3f; //只按方向键速度为3
/// <summary>
/// 跳跃
/// </summary>
public float jumpHeight = 0;
//判断是否为跳跃状态
private bool boolJump = false;
void Update()
{
//Debug.Log(photonView.Owner.NickName);
//判断是否是本机玩家 只能操作本机角色
if (photonView.IsMine)
{
if (isDie == true)
{
return;
}
//在Update函数中如果判断为本机操控的玩家就执行更新位置的方法
UpdatePosition();
UpdateRotation();
InputCtl();
}
else
{
UpdateLogic();
}
}
void FixedUpdate()
{
body.velocity = new Vector3(dir.x, body.velocity.y, dir.z) + Vector3.up * jumpHeight;
jumpHeight = 0f;//初始化跳跃高度
}
//更新位置
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);
//当按下空格键进行跳跃
if (Input.GetKeyDown(KeyCode.Space))
{
if (boolJump == false)
{
boolJump = true;
//设定一个跳跃时间间隔,不然就能一直往上跳了
Invoke("something", 1.0f);
//执行跳跃方法
Jump();
}
}
//加速跑 当同时按住Shift 和 方向键
if (Input.GetKey(KeyCode.LeftShift) && (dir.x != 0 || dir.y != 0 || dir.z != 0))
{
body.MovePosition(transform.position + dir * Time.deltaTime * 10);
}
//当抬起 Shift 键
else if (Input.GetKeyUp(KeyCode.LeftShift))
{
body.MovePosition(transform.position + dir * Time.deltaTime * MoveSpeed);
}
}
void something() {
boolJump = false;
}
//跳跃方法
void Jump()
{
jumpHeight = 5f;
}
二、玩家自定义输入昵称
2.1 给昵称赋值
首先说一下在PUN插件中给玩家昵称赋值的代码,赋好值之后我们只要进行获取就可以了
//playerNameInput.text —— 玩家手动输入的名字
PhotonNetwork.NickName = playerNameInput.text;
2.2 实现
UI方面小编就比较省事了,输入昵称和输入房间号用的同一个UI界面。在登录UI的LoginUI脚本中,点击开始游戏按钮我们不让它直接进行连接,先让它跳转到输入昵称的UI界面中。
//登录界面
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);
}
public void onStartBtn()
{
//弹出输入玩家昵称的UI界面 CreatePlayerUI
Game.uiManager.ShowUI<CreatePlayerUI>("CreatePlayerUI");
}
public void onQuitBtn()
{
Application.Quit();
}
}
CreatePlayerUI脚本中进行连接并通过PhotonNetwork.NickName给玩家昵称赋值
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;
public class CreatePlayerUI : MonoBehaviour,IConnectionCallbacks
{
private InputField playerNameInput; //玩家名称
void Start()
{
transform.Find("bg/title/closeBtn").GetComponent<Button>().onClick.AddListener(onCloseBtn);
transform.Find("bg/okBtn").GetComponent<Button>().onClick.AddListener(onStartBtn);
playerNameInput = transform.Find("bg/InputField").GetComponent<InputField>();
//先随机一个玩家名称
playerNameInput.text = "Player_" + Random.Range(1, 9999);
}
public void onStartBtn()
{
Game.uiManager.ShowUI<MaskUI>("MaskUI").ShowMsg("正在连接服务器...");
//连接pun2服务器
PhotonNetwork.ConnectUsingSettings(); //成功后会执行OnConnectedToMaster函数
}
//关闭按钮
public void onCloseBtn()
{
Game.uiManager.CloseUI(gameObject.name);
}
//OnEnable()每次激活组件都会调用一次
private void OnEnable()
{
PhotonNetwork.AddCallbackTarget(this); //注册pun2事件
}
//OnDisable()每次关闭组件都会调用一次 与 OnEnable() 相对
private void OnDisable()
{
PhotonNetwork.RemoveCallbackTarget(this); //注销pun2事件
}
//连接成功后执行的函数
public void OnConnectedToMaster()
{
//关闭所有界面
Game.uiManager.CloseAllUI();
Debug.Log("连接成功");
//显示大厅界面
Game.uiManager.ShowUI<LobbyUI>("LobbyUI");
//执行昵称赋值操作
PhotonNetwork.NickName = playerNameInput.text;
}
//断开服务器执行的函数
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)
{
}
public void OnConnected()
{
}
}
三、玩家昵称同步到房间列表
3.1 获取全部玩家
PUN插件中从服务器获取房间里的全部玩家:
//从服务器遍历房间里的所有玩家项
for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
{
Player p = PhotonNetwork.PlayerList[i];
//打印出玩家昵称,看看我们赋没赋值成功
Debug.Log("NickName:" + p.NickName);
}
在PUN插件的Player类中,NickName(玩家昵称)和ActorNumber(玩家编号)字段是Player类源码中定义的字段,如果我们开发者需要自定义字段可以通过这样来自定义:Demo中玩家是否准备就是用下面的方式来定义的
3.2 自定义Player中的字段
同步自定义字段:
using ExitGames.Client.Photon;
Hashtable props = new Hashtable() { { "IsReady", true } };
PhotonNetwork.LocalPlayer.SetCustomProperties(props);
获取自定义字段:
foreach (Player p in PhotonNetwork.PlayerList)
{
print(p.NickName);
object isPlayerReady;
if (p.CustomProperties.TryGetValue("IsReady", out IsReady))
{
print((bool)IsReady ? "当前玩家已准备好" : "当前玩家未准备好");
}
}
//获取所有自定义字段
Debug.Log(玩家Player.CustomProperties.ToStringFull());
3.3 实现
- 获取房间内所有的玩家信息包括昵称和准备状态
- 将昵称和准备状态显示到UI界面中
在RoomUI脚本中,先获取房间内的所有玩家,对应的每一个玩家就会生成一个新的RoomItem。
我们给房间列表成员RoomItem中添一个玩家昵称的字段,用来获取玩家进入游戏输入的昵称并展示在UI界面中。
public int owerId; //玩家编号
public bool IsReady = false; //是否准备
public string playerName; //玩家名称
RoomUI脚本:
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];
Debug.Log("NickName:" + p.NickName);
//获取房间中的玩家后,每一个玩家生成对应的一个Item
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; //玩家编号
item.playerName = p.NickName; //玩家昵称
item.playerNameText(item.playerName); //让玩家昵称显示到UI界面中
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)
{
}
}
RoomItem脚本:
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;
public class RoomItem : MonoBehaviour
{
public int owerId; //玩家编号
public bool IsReady = false; //是否准备
public string playerName; //玩家名称
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 ? "已准备" : "未准备";
}
public void playerNameText(string playerName)
{
transform.Find("Name").GetComponent<Text>().text = playerName;
}
}
四、计分板功能的实现
4.1 设置玩家分数
//设置玩家分数
PhotonNetwork.LocalPlayer.SetScore(0);
在PUN中有自带的设置玩家分数功能,我们来看一下源码:SetScore、AddScore、GetScore
通过方法的命名我们就知道它们分别是设置分数、增加分数、获取分数, 不过小编这里只用了设置和获取(*/ω\*),分数更新后把原有的重新设置覆盖掉了。
知道了原理我们来实现计分板功能。
4.2 实现
首先计分板的UI我还是用的房间界面的UI改一下。
先来理一下思路 ——
- 当识别为本机玩家操作后,按住Tab键弹出该界面,松开关掉界面
- 计分板要获取房间内所有玩家信息:昵称、分数
- 当本机玩家射击击中其他玩家后,本机玩家分数自增
- 玩家分数更新后再次按下Tab键时要更新UI中的分数
- 当游戏房间中有玩家离开对应计分板也会删掉对应的玩家信息
在PlayerController中
private int Score = 0; //定义分数变量 —— 重点!!!!
void Update()
{
//Debug.Log(photonView.Owner.NickName);
//判断是否是本机玩家 只能操作本机角色 —— 重点!!!!
if (photonView.IsMine)
{
if (isDie == true)
{
return;
}
UpdatePosition();
UpdateRotation();
//判断为本机玩家后执行按键操作方法 —— 重点!!!!
InputCtl();
}
else
{
UpdateLogic();
}
}
//角色操作
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.Escape))
{
Application.Quit();
}
//持续按下按键,查看计分板
if (Input.GetKey(KeyCode.Tab))
{
//打开计分板界面 —— 重点!!!!
Game.uiManager.ShowUI<ScoreboardUI>("ScoreboardUI");
//执行更新分数方法 —— 重点!!!!
Game.uiManager.ShowUI<ScoreboardUI>("ScoreboardUI").UpDateScore();
// foreach (Player p in PhotonNetwork.PlayerList)
// {
// Debug.Log("NickName:" + p.NickName);
// Debug.Log("GetScore:" + p.GetScore());
// }
}
//当Tab键抬起
else if(Input.GetKeyUp(KeyCode.Tab))
{
//关闭计分板界面 —— 重点!!!!
Game.uiManager.CloseUI("ScoreboardUI");
}
if (Input.GetKeyDown(KeyCode.Q))
{
ani.Play("Grenade_Throw");
}
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);
//获取本机玩家 —— 重点!!!!
Player p = PhotonNetwork.LocalPlayer;
//射线检测 鼠标中心点发送射线
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("射到角色");
//当本机玩家射中其他玩家时,把获取的本机玩家作为参数传递到GetHit方法中 —— 重点!!!!
hit.transform.GetComponent<PlayerController>().GetHit(p);
}
photonView.RPC("AttackRpc", RpcTarget.All); //所有玩家执行 AttackRpc 函数
}
[PunRPC]
public void AttackRpc()
{
gun.Attack();
}
//同步所有角色受伤 p —— 代表本机玩家
public void GetHit(Player p)
{
if (isDie == true)
{
return;
}
//同步所有角色受伤
photonView.RPC("GetHitRPC", RpcTarget.All);
//本机玩家得分自增并同步给服务器 —— 重点!!!!
Score += 1;
p.SetScore(Score);
}
在ScoreboardUI中,和RoomUI的脚本逻辑差不多
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Pun.UtilityScripts;
using Photon.Realtime;
using UnityEngine.UI;
public class ScoreboardUI : MonoBehaviour
{
Transform startTf;
Transform contentTf;
GameObject roomPrefab;
public List<ScoreItem> roomList;
// Start is called before the first frame update
void Awake()
{
roomList = new List<ScoreItem>();
contentTf = transform.Find("bg/Content");
//房间列表玩家成员
roomPrefab = transform.Find("bg/roomItem").gameObject;
}
void Start()
{
//从服务器获取房间里的玩家项
for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
{
Player p = PhotonNetwork.PlayerList[i];
CreateRoomItem(p);
}
}
//生成玩家
public void CreateRoomItem(Player p)
{
GameObject obj = Instantiate(roomPrefab, contentTf);
obj.SetActive(true);
ScoreItem item = obj.AddComponent<ScoreItem>();
item.owerId = p.ActorNumber;
item.playerName = p.NickName;
item.playerNameText(item.playerName);
item.Score = p.GetScore();
item.playerScoreText(item.Score);
roomList.Add(item);
}
//执行更新房间内玩家分数的操作 —— 重点!!!!
public void UpDateScore()
{
for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
{
Player p = PhotonNetwork.PlayerList[i];
ScoreItem item = roomList.Find((ScoreItem _item) => { return p.ActorNumber == _item.owerId; });
if (item != null)
{
item.playerName = p.NickName;
item.playerNameText(item.playerName);
item.Score = p.GetScore();
item.playerScoreText(item.Score);
Debug.Log("NickName:" + p.NickName + "GetScore:" + p.GetScore());
Debug.Log("::::::::::::::::::::::::::::::::::::::::::::::::::");
}
}
}
//删除离开房间的玩家
public void DeleteRoomItem(Player p)
{
ScoreItem item = roomList.Find((ScoreItem _item) => { return p.ActorNumber == _item.owerId; });
if (item != null)
{
Destroy(item.gameObject);
roomList.Remove(item);
}
}
//房间里的其他玩家离开房间
public void OnPlayerLeftRoom(Player otherPlayer)
{
DeleteRoomItem(otherPlayer);
}
private void OnEnable()
{
PhotonNetwork.AddCallbackTarget(this);
}
private void OnDisable()
{
PhotonNetwork.RemoveCallbackTarget(this);
}
}
ScoreItem的脚本用来把玩家信息和分数显示到计分板上
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Pun.UtilityScripts;
using Photon.Realtime;
public class ScoreItem : MonoBehaviour
{
public int owerId; //玩家编号
public int Score; //玩家分数
public string playerName; //玩家名称
public void playerNameText(string name)
{
transform.Find("Name").GetComponent<Text>().text = name; //PhotonNetwork.LocalPlayer.NickName;
}
public void playerScoreText(int score)
{
transform.Find("Score").GetComponent<Text>().text = score.ToString();//PhotonNetwork.LocalPlayer.GetScore().ToString();
}
}
完成任务,真的很喜欢这个Demo,以后有时间还会继续优化的。今天先到这里,拜拜┏(^0^)┛