https://study.163.com/course/courseMain.htm?share=1&shareId=8348227&courseId=1004507022html
变量名使用Camel驼峰命名法:首字母小写,其他单词首字母大写canvas
命名空间、类和方法名使用Pascal命名法:全部单词首字母大写服务器
若是使用到英文单词的缩写,则所有大写:如ID网络
https://dashboard.photonengine.com/zh-cndom
为何使用Photon服务器?
Photon服务器能够很好地实现游戏房间的匹配功能 --
进入匹配房间,若是没有匹配的房间,则新建房间
当房间人数达到必定数量,则会开始游戏ide
Photon插件:AssetStore下载:全名 Photon Unity Networking Classic - Free
Import以后,会弹出一个设置向导(向导也可经过Window->PhotonUnityNeworking->PUN Wizard打开)
点击Cloud DashBoard Login后打开网页,登陆优化
建立Photon App:
1. Photon Type = Photon PUN
2. 点击建立动画
将建立好的App的AppID复制,在Unity->Window->PhotonUnityNetworking->PUN Wizard中点击Locate PhotonServerSettings,
Hosting选择PhotonCloud (这里Region选择Jp,延迟比较低),粘贴AppID,Protocol选择Udp
Client Settings的Pun Logging选择Fullui
经过代码与光子服务器进行链接this
https://doc.photonengine.com/en-us/pun/current/getting-started/pun-intro
命名空间Photon.PunBehaviour,PhotonNetwork.ConnectUsingSettings(string gameVersion)
参数gameVersion表示客户端版本号,用于分离版本
建立脚本Network.cs,在Awake()中写上PhotonNetwork.ConnectUsingSettings("0.0.1");
将脚本挂载空游戏物体上,运行
与服务器链接成功
地形系统 -- Terrain
新建Terrain,发现没有所需的地形Texture
从商店导入Standard Assets素材包
人物模型https://pan.baidu.com/s/1An5Ro8pHMdjgnPAoBoRA_A,nubq
Terrain建立 -- 略
使用Standard Assets中的RigidBodyFPSController,用于控制人物,并导入人物模型(RifleAnimsetPro--Models) -- 略
将枪模型RiflePlaceholder放在人物模型手上,并经过Animator实现动做
开枪功能 -- 在主角模型上挂脚本WeaponController.cs
1. 当前弹夹中子弹数;2. 开枪动做播放;3. 开枪音效播放;4. Update()中检测是否按下鼠标左键
5. 弹夹中子弹数不足则卡壳,播放卡壳音效
换弹夹功能 --
1. Update()中检测是否按下R键;2. 子弹总数和当前弹夹中子弹数;3. 换弹动做播放;4. 换弹音效播放
开镜功能 --
Camera的Field of View参数 即为开镜效果
准星 -- canvas - image实现
枪口特效 --
RifleAnimsetPro/Particles/MuzzleFlash
放置于枪口,并在Shoot中控制播放
private ParticleSystem m_ShootFx;
m_ShootFx.Play();
射击功能的弹道 --
暂时使用射线的方式实现射击功能,以后会优化改成真正的有弹道的子弹
// 射线模拟子弹 RaycastHit raycastHit; if (Physics.Raycast(m_MainCamera.transform.position, m_MainCamera.transform.forward, out raycastHit, m_FarestBulletReachableDistance)) { Debug.LogError(raycastHit.transform + " Shot"); }
匹配UI -- 在net scene下制做一个按钮,实现点击加入房间的功能
加入房间相关Photon API:
让Net.cs类继承自Photon.PunBehaviour
加入房间: PhotonNetwork.JoinRandomRoom();
加入房间失败时的回调方法: OnPhotonRandomJoinFailed(object[] codeAndMsg) {}
建立房间: PhotonNetwork.CreateRoom(string roomName);
// 建立或加入房间: PhotonNetwork.JoinOrCreateRoom(string roomName);
没有试过该API,无需知道该名字的房间是否已经存在,加入或建立它
建立房间成功的回调方法: OnCreatedRoom() {}
加入房间成功时的回调方法: OnJoinedRoom() {}
有玩家成功链接入/加入房间的回调方法: OnPhotonPlayerConnected(PhotonPlayer newPlayer) {}
房间内全部玩家是否同步场景的属性: PhotonNetwork.automaticallySyncScene == true;
// defines if all clients in a room should load the same level/scene as the Master client (if that used PhotonNetwork.LoadLevel())
判断当前客户端是否为主机(房主): PhotonNetwork.isMasterClient
加载场景: PhotonNetwork.LoadLevel(string sceneName); // 好比 PhotonNetwork.LoadLevel("main");
如何使用:
1. 将以前在Start()中调用的PhotonNetwork.ConnectUsingSettings("0.0.1"); 注册到一个Connect按钮上,成功链接上服务器后才启用加入房间的按钮
2. 通常来讲,随机加入房间若是失败时,则自动建立一个房间
3. 一个房间中玩家数足够时,则自动开始游戏
4. 只有主机才须要加载场景,其余的客户端须要设置同步场景属性
功能扩展: 能够再加一个DropDown来控制房间玩家数(Photon好像最大支持20人同房)
一场游戏里有不少个玩家,这些玩家的模型和位置都须要同步,所以一个场景中须要多个士兵模型
玩家脚本 -- PlayerUnit.cs
1. 若是玩家不是客户端控制的玩家,则不须要开启模型中的摄像机
2. 不是客户端控制的玩家,有一些组件是不须要开启的(包括1中的摄像机、角色控制脚本、武器脚本)
3. 有关同步的API: PhotonView
PlayerUnit脚本挂载角色身上,角色作成一个预制体,用于动态生成
用于管理生成玩家的脚本 -- PlayerGenerator.cs
在游戏场景main scene加载完成时,进行玩家的生成(PhotonNetwork.Instantiate())
-- SceneManager.sceneLoaded 注册事件
PhotonNetwork.Instantiate(string prefabName, Vector3 position, Quaternion rotation, byte group)
-- 经过网络,实例化预制体,预制体须要位于Resources文件夹下
-- group指的是PhotonView的组,填0便可
在须要经过Photon进行网络同步的物体(角色)上挂载脚本
PhotonView:
PhotonTransformView:
勾选须要同步的参数
须要将PhotonTransformView赋值给PhotonView的OvservedComponents属性中
表示须要监测并同步PhotonTransformView中勾选的参数
将角色作成预制体,并在场景中删除
玩家血量属性存在玩家信息类PlayerInfo中
public int hp = 100;
在武器脚本中存放伤害值
public int damage = 50;
在开火射线检测代码块断定是否打中玩家
思路: 经过玩家tag判断
若是是玩家,则玩家受伤,并须要将伤害同步给全部客户端
API: PhotonView.RPC(string methodName, PhotonPlayer targetPlayer, params object[] parameters)
Call a RPC method of this GameObject on remote clients of this room( or on all, including this client)
methodName: 调用的同步方法名 the name of the fitting method which has the RPC attribute
targetPlayer: 须要同步给的客户端The group of targets and the way RPC gets sent
parameters: 传入methodName方法的参数
if(raycastHit.collider.tag == "Player") { // 打到角色 PhotonView pv = raycastHit.collider.GetComponent<PhotonView>(); pv.RPC("GetDamage", PhotonTargets.All, m_Damage); }
注意,在定义methodName方法时,须要写上 [PunRPC] -- 表示这是一个须要被实时同步的方法
并继承自接口 IPunObserable的OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {}
在该方法中进行血量的同步
[PunRPC] public void GetDamage(int damage) { m_CurrentHp -= damage; } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { // 会发送给全部客户端(包括本身) if (stream.isWriting) { // 若是是写入者 stream.SendNext(this.m_CurrentHp); } else { // 若是是读出者 this.m_CurrentHp = (int)stream.ReceiveNext(); } }
在Player预制体下建立Canvas,用Slider制做简易血条(玩家本身的血条,位于屏幕下方)
血条的屏幕分辨率自适应:
将Canvas Scaler的UI Scale Mode设为Scale With Screen Size
将Match调至Height处,表示以Height为基础作的自适应
在PlayerInfo中控制Slider的value值
在同步方法中同步Slider的value值
[PunRPC] m_CurrentHp -= damage; if(m_CurrentHp < 0) { m_CurrentHp = 0; } slider.value = m_CurrentHp; } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { // 会发送给全部客户端(包括本身) if (stream.isWriting) { // 若是是写入者 stream.SendNext(this.m_CurrentHp); stream.SendNext(this.slider.value); } else { // 若是是读出者 this.m_CurrentHp = (int)stream.ReceiveNext(); this.slider.value = (int)stream.ReceiveNext(); // 我的理解这里没必要再传slider.value的值,直接在m_CurrentHp变化的地方触发更新便可 } }
视频教程中用的方法是:每个PlayerUnit都配备一个Slider用于显示血条,可是在PlayerUnit的ComponentsToBeHiden的列表里加上该血条
我的作法: 只作一个血条Slider,用PhotonView.isMine判断是否为本客户端的玩家,在PlayerManager(做为总控制(全部玩家)的脚本中)控制HpBar
https://docs.unity3d.com/Manual/InverseKinematics.html ---- 看一看,介绍地很详细
https://www.jianshu.com/p/7aec3699f29c -- 很详细
完善玩家视角变更(抬高压低)时,枪口跟随的功能
IK: Inverse Kinematics -- 反向动力学
通常而言,多数动画是经过旋转骨架关节的角度来实现的,这个称为 Forward Kinematics
而IK指的是从下至上的驱动
是根据骨骼的终节点来推算其余父节点的位置的一种方法。好比经过手的位置推算手腕、胳膊肘的骨骼的位置。
好比设置好手部位置后,角色起身时手部会保持不动,而小臂和大臂的古河会自动旋转到合适角度;
或者在脚步踩入地面的行走动画(沼泽地)状况下,在运行时经过调整IK target来实现角色在不平坦的地面行走的效果
仅支持配置正确的人形角色 (supported in Mecanim for any humanoid character with a correctly configured Avatar)
人形角色指的是:预制体设置的Rig->AnimationType=Humanoid
IK的使用
1. 人物模型预制体的设置Rig->AnimationType设为Humanoid
设置为Humanoid后,会自动在人物模型预制体下生成一我的物骨骼CharacterAvatar
2. 在动画控制器中的Layers Pane勾选IK
3. 在Animator组件中勾选Apply Root Motion
To set up IK for a character, you typically have objects around the scene that a character interacts with, and then set up the IK through script, in particular, Animator functions like SetIKPositionWeight, SetIKRotationWeight, SetIKPosition, SetIKRotation, SetLookAtPosition, bodyPosition, bodyRotation
4. Animator组件中的Avatar赋值为Humanoid自动生成的CharacterAvatar(这一步骤作了以后 播放动画的问题很大)
5. 实现IK实际功能的脚本 (即https://docs.unity3d.com/Manual/InverseKinematics.html 中的案例)
using UnityEngine; using System; using System.Collections; [RequireComponent(typeof(Animator))] public class IKController : MonoBehaviour { protected Animator animator; public bool ikActive = false; public Transform rightHandObj = null; public Transform lookObj = null; void Start() { animator = GetComponent<Animator>(); } //a callback for calculating IK void OnAnimatorIK() { if (animator) { //if the IK is active, set the position and rotation directly to the goal. if (ikActive) { // 这里只须要用到这个 // Set the look target position, if one has been assigned if (lookObj != null) { // 看向某个目标点时,各个部位旋转的比重 animator.SetLookAtWeight(1, 1, 1, 1); animator.SetLookAtPosition(lookObj.position); } // Set the right hand target position and rotation, if one has been assigned if (rightHandObj != null) { animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1); animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandObj.position); animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandObj.rotation); } } //if the IK is not active, set the position and rotation of the hand and head back to the original position else { animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 0); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 0); animator.SetLookAtWeight(0); }}}}
6. 在Player下的Camera下建立空物体(由于须要看向的位置是与摄像机有关的,因此做为Camera的子物体)
将该空物体放置在Camera前的合适位置,做为枪口指向的点
并将该空物体赋值给IKController.lookObj
素材: pan.baidu.com/wap/init?surl=sA1JWo39facMXIa4M97dFQ 5n1b
导入模型,并加上四个WheelCollider
注:使用WheelCollider的必要条件是,父物体上须要挂有RigidBody组件