emm关于聊天功能的闲话
聊天功能也是使用PhotonChat提供的API,比较方便的是参考demo,发消息的逻辑代码直接拿来用就好了。要注意目前pun版本的chat服务只有eu服务器。
然后本项目的聊天功能中UI是自主开发的。仿照英雄联盟的聊天样式,游戏内不会遮挡,并且一定时间没有消息后自动隐藏…
道具战游戏简介
部分资源是Unity官方商店(比如人物)、还有部分资源来自其他素材网站。
截图
生成角色。
随着时间场上生成了不少道具。
捡了一个隐身、导弹、地雷,还有瞬移突进技能。
按2发射导弹打坏木箱,附带粒子特效。
游戏功能
人物–控制系统、生命值系统、道具捡拾与使用系统。
整个场景–道具生成系统(道具悬浮效果)、草丛隐身机制。
道具–不同道具不同功能,其中有隐身道具要和草丛的隐身联动起来。
道具计划制作五个(导弹、地雷、恢复、隐身、疾步),额外制作一个冲刺位移技能。
其中隐身道具和进入草丛“隐身”的实现方式详见下一篇文章。
生成数有最大限制以控制游戏以适中的节奏进行,同时避免过多道具占用cpu资源。
但是currentCount的–有问题,不清楚原因。
关键代码
全局
PropsManager.cs
:场景中道具生成器。
/// <summary>
/// 控制道具的生成
/// </summary>
public class PropsManager : MonoBehaviour
{
public enum PropsType
{
None,
MissileProp, //炮弹
LandmineProp, //地雷
CloakProp,
HealthBottleProp, //血瓶
SpeedUpProp,//疾步
FlashSkill
}
private string[] propsPrefabNameWithRandom
= {"DroppedMissile", "DroppedMissile", "DroppedLandMine", "DroppedLandMine", "DroppedBottle", "DroppedRandomBox" };
public static int mapSizeCanSpawn = 20;//default 20,用于随机范围
public float spawnInterval = 4;//间隔:?秒一次
private float currentTime = 4;
public int spawnCountOneTime = 4;
public int maxSpawnCountPermit = 20;//限制最大数量---弱限制,可19到19 + spawnCountOneTime。
private static int currentCountInScene = 0;//当前场景中存在的数量
private void Start()
{
if(!PhotonNetwork.IsMasterClient)
{
return;
}
maxSpawnCountPermit = PhotonNetwork.PlayerList.Length * 15;
}
private void Update()
{
//生成道具
if(PhotonNetwork.IsMasterClient && PropsGameManager.instance.isGameStart)
{
//计时
if(currentTime > 0)
{
currentTime -= Time.deltaTime;
}
else
{
//判断道具数是否到达上限
//if (currentCountInScene < maxSpawnCountPermit)
//{
for (int i = 0; i < spawnCountOneTime; i++)
{
/*
PhotonNetwork.Instantiate(
Const.PUN_PROPS_GAME_PREFABS_BASEPATH
+ propsPrefabNameWithRandom[Random.Range(0, propsPrefabNameWithRandom.Length)]
, propsSpawnPoints[Random.Range(0, propsSpawnPoints.Length)].position
, Quaternion.identity);
*/
Vector3 pos = new Vector3(Random.Range(-18f, 18f), 1, Random.Range(-18f, 18f));
PhotonNetwork.Instantiate(Const.PUN_PROPS_GAME_PREFABS_BASEPATH
+ propsPrefabNameWithRandom[Random.Range(0, propsPrefabNameWithRandom.Length)]
, pos
, Quaternion.identity);
currentCountInScene++;
}
//}
currentTime = spawnInterval;
}
}
}
public static void DecreaseCurrentCount()
{
currentCountInScene--;//TODO:线程是否安全
}
}
悬浮物道具
DroppedProps.cs
:掉落物形式的道具(可捡状态,包含悬浮动作、保证生成在空旷处)
public class DroppedProps : MonoBehaviour//用dotween
{
public PropsManager.PropsType propType = PropsManager.PropsType.HealthBottleProp;
private Vector3 startPos;
void Start()
{
if(PhotonNetwork.IsMasterClient)
{
TestCollision();
}
Move();
}
//出生检测
//静止物体没有刚体,通过物理检测来决定重新生成,同修改DroppedRandomProps脚本
private void TestCollision()
{
float radius = 0.8f;
Vector3 explosionPos = transform.position;
Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);
foreach (Collider hit in colliders)
{
if (hit.gameObject == this.gameObject)
{
Debug.Log("self collision");
continue;
}
if (hit.gameObject.tag.Equals("Barrier") || hit.gameObject.tag.Equals("WoodenBox")
|| hit.gameObject.tag.Equals("Grass") || hit.gameObject.tag.Equals("DroppedProps"))
{
float sphereRadius = (float)PropsManager.mapSizeCanSpawn;
Vector3 pos = new Vector3(Random.Range(-sphereRadius, sphereRadius), 1, Random.Range(-sphereRadius, sphereRadius));
if(PhotonNetwork.IsMasterClient)
{
Debug.Log("发生碰撞self 刷新");
PhotonNetwork.Instantiate(Const.PUN_PROPS_GAME_PREFABS_BASEPATH + gameObject.name.Replace("(Clone)", ""), pos, Quaternion.identity);
PhotonNetwork.Destroy(gameObject);
return;
}
}
}
}
//悬浮
private void Move()
{
startPos = transform.position;
transform.DOPath(new Vector3[] { startPos, transform.position + new Vector3(0, 0.5f, 0), startPos }, 2)//Vector3.up//new Vector3(0,0.5f,0)
.SetLoops(-1, LoopType.Restart);
transform.DOBlendableLocalRotateBy(new Vector3(0, 360, 0), 4f, RotateMode.FastBeyond360)//.DORotate(new Vector3(0, 360, 45), 3f, RotateMode.WorldAxisAdd)
.SetLoops(-1, LoopType.Restart);//循环设置为-1为一直
}
//销毁写在人物里面,背包满了道具碰到不销毁
}
DroppedRandomProps.cs
:因为隐身和疾步一些道具没有合适的3d资源。统一用一个“问号箱子”,也就是随机道具。
和DroppedProps.cs
基本一样,多了一个随机确定被捡的道具。
实际道具
PropMissile.cs
:导弹道具(施放道具,非掉落物形式,为玩家使用后的真实导弹物体)
包含碰撞(爆炸)检测。和初始化问题。初始化时主要为调整导弹朝向。
(导弹模型默认导弹头是z轴正向的,所以要把z轴正向转向发射方向)
public class PropMissile : MonoBehaviour
{
public Player Owner { get; private set; }
public GameObject explosionParticle;
public void Start()
{
Destroy(gameObject, 3.0f);
}
public void OnTriggerEnter(Collider collision)
{
Debug.Log(collision.gameObject.name + "eee");//TODO:有时候会碰到自己?
if (collision.gameObject.tag.Equals("DroppedProps") || collision.gameObject.tag.Equals("Grass"))
{
return;
}
if (collision.gameObject.tag.Equals("Player") && collision.gameObject.GetComponent<PhotonView>().Owner == Owner)
{
return;
}
float radius = 3.0f;
Vector3 explosionPos = transform.position;
Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);
bool clipIsPlayed = false;//如果破坏掉木箱,此bool控制只播放一次音效
foreach (Collider hit in colliders)
{
if (hit.gameObject.tag.Equals("Player"))//打到玩家
{
//TODO:不能打到自己
if(Owner.NickName.Equals(hit.gameObject.GetComponent<PhotonView>().Owner.NickName))
{
continue;
}
PropsPlayerController ppc = hit.gameObject.GetComponent<PropsPlayerController>();
PropsPlayerHealth pph = hit.gameObject.GetComponent<PropsPlayerHealth>();
PropsPlayerPropController pppc = hit.gameObject.GetComponent<PropsPlayerPropController>();
//玩家击飞
if (hit.attachedRigidbody)
{
hit.attachedRigidbody.AddExplosionForce(500, explosionPos, radius);
}
//掉血\动画\失控
if (ppc != null)
{
Debug.Log("调用switch方法");
pph.GetDamage(1, 1);
pppc.DoAnimationWithEffect(1);//动画与失控
}
//加分先不写
}
else if (hit.gameObject.tag.Equals("WoodenBox"))//打到木箱
{
//音效,木箱摧毁
if(!clipIsPlayed)
{
clipIsPlayed = true;
AudioManager.instance.WoodBreak(transform.position);//只播放一次
}
Destroy(hit.gameObject);
}
}
AudioManager.instance.MissileBomb(transform.position);
//TODO:爆炸特效
GameObject go = GameObject.Instantiate(explosionParticle, transform.position, Quaternion.identity);
Destroy(go, 1);
Destroy(gameObject);
}
public void Initialize(Player owner, Vector3 playerToMouse)//, Vector3 originalDirection , flaot lag
{
Owner = owner;
/*
Vector3 originalDirection = rotation * Vector3.forward;//平面,y=0,非标准化,xz是比例决定方向,数字绝对值最大为1
transform.forward = originalDirection;
*/
//transform.rotation = Quaternion.LookRotation(playerToMouse);
//TODO:Quaternion.FromToRotation(vec1, vec2)//t.p, floor(mousePoint)
Quaternion rotation = transform.rotation;
Vector3 fromDir = rotation * Vector3.up;
Vector3 axis = Vector3.Cross(fromDir, playerToMouse).normalized;
float angle = Vector3.Angle(fromDir, playerToMouse);//俩向量角度
transform.rotation = Quaternion.AngleAxis(angle, axis) * rotation;
Rigidbody rigidbody = GetComponent<Rigidbody>();
rigidbody.velocity = playerToMouse.normalized * 20;
//rigidbody.position += rigidbody.velocity * lag;
}
}
其他道具就不介绍了。比如地雷的爆炸基本和导弹一样。血瓶恢复也好实现。
人物脚本
嗯比较复杂的是关于人物的脚本:
人物控制:涉及到Unity的一大利器:Ray,此应用为获取鼠标所指的方向,发射导弹时是由鼠标指向控制的。
PlayerController.cs
:
其中的SetInvisible
和SetVisible
方法是与隐身有关的方法(下一篇文章中的关闭渲染器方式)。
private void Update()
{
playerUICanvas.transform.rotation = Camera.main.transform.rotation;//TODO
}
private void FixedUpdate()//获取玩家对于枪械的输入-鼠标左键或R键、人物移动
{
//playerUICanvas.transform.rotation = Camera.main.transform.rotation;//TODO:分离
if (!photonView.IsMine)//非自己退出
{
return;
}
if (transform.position.y < 0)
{
transform.position = new Vector3(0, 2, 0);
}
/*
if (!canControll)
{
return;
}
*/
if(!canControll)
{
return;
}
Move();
Turning();
}
#endregion
#region public人物方法-- 受伤、生命、移动、显示、动画
public void Move()
{
Vector3 moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
Vector3 moveVelocity = moveInput.normalized * moveSpeed;
thisRg.MovePosition(thisRg.position + moveVelocity * Time.fixedDeltaTime);
if(moveVelocity.magnitude > 0) animator.SetFloat("speed", 5);
else animator.SetFloat("speed", -1);
}
public void Turning()
{
Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit floorHit;
if (Physics.Raycast(camRay, out floorHit, 30f, floorMask))
{
Vector3 playerToMouse = floorHit.point - this.transform.position;
playerToMouse.y = 0f;
//y不是0,朝向看向地板
Quaternion newRotatation = Quaternion.LookRotation(playerToMouse);
//this.transform.DORotateQuaternion(newRotatation, 0.5f);//TODO:用刚体解决画布问题
thisRg.rotation = newRotatation;//.MoveRotation(newRotatation);
}
}
public void SetInvisible()
{
//把孩子的render关了
transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().enabled = false;
transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().enabled = false;
playerUICanvas.gameObject.SetActive(false);
}
public void SetVisible()
{
//把孩子的render打开
transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().enabled = true;
transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().enabled = true;
playerUICanvas.gameObject.SetActive(true);
}
//人物死后取消碰撞//TODO:生命值剩一个检测
public void CloseCollider()
{
GetComponent<BoxCollider>().enabled = false;
}
#endregion
PlayerHealth.cs
:
生命值系统,此游戏生命值为UI直观显示。如截图那里人物头顶上包含昵称显示与生命值显示。由五个独立的红色小图形作为血条(五格血)。
这是包含了pun同步的脚本
public class PropsPlayerHealth : MonoBehaviour
{
[Header("==Health==")]
int maxHealth = 5;
int currentHealth;//=max
[Header("==UI==")]
public GameObject healthPanel; //血条面板
public Text infoText;
[Header("==Other==")]
public PropsPlayerController ppc;
private PhotonView photonView;
private void Start()
{
currentHealth = maxHealth;
photonView = this.GetComponent<PhotonView>();
infoText = GameObject.Find("UI").transform.GetChild(0).GetComponent<Text>();
}
/// <summary>
/// damageSource = 1:导弹 2:地雷
/// </summary>
/// <param name="damageValue"></param>
/// <param name="damageSource"></param>
public void GetDamage(int damageValue, int damageSource)//TODO:此游戏damageValue用不到,伤害设计为1
{
if (!photonView.IsMine)
{
return;
}
if (currentHealth <= 0)
{
return;
}
//TODO:如果排除他人执行代码则需要rpc执行血条更新和音效等
currentHealth--;
photonView.RPC("DecreaseHealth", RpcTarget.All, currentHealth);
if (currentHealth <= 0)//死亡
{
ppc.canControll = false;
//RPC
photonView.RPC("PlayerDie", RpcTarget.All, PhotonNetwork.LocalPlayer.NickName, damageSource);
//摄像头位置
Camera.main.gameObject.GetComponent<AutoFollow>().enabled = true;
//玩家死亡
PhotonNetwork.LocalPlayer.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { Const.PLAYER_LIVES, 0 } });
//仅取消渲染而不销毁,留着脚本执行rpc等
ppc.SetInvisible();//TODO:是否留有碰撞
//PhotonNetwork.Destroy(gameObject);
}
}
public void healthAdd()
{
if (currentHealth < maxHealth)
{
//通知所有人执行加血的UI
photonView.RPC("AddHealth", RpcTarget.All, currentHealth);//加血
currentHealth++;
}
}
#region rpc
[PunRPC]
public void AddHealth(int currentHealth)//*******TODO:currentHealth++不需要出现在这里?//涉及到恢复
{
if (currentHealth >= 5)
{
return;
}
healthPanel.transform.GetChild(currentHealth).GetComponent<Image>().color = Color.red;
//currentHealth++;
}
[PunRPC]
public void DecreaseHealth(int currentHealth) //减少血量
{
AudioManager.instance.PropsGameGetDamage(transform.position);
healthPanel.transform.GetChild(currentHealth).GetComponent<Image>().color = Color.gray;//TODO
}
[PunRPC]
public void PlayerDie(string nickname, int damageSource)
{
AudioManager.instance.PlayerDie(transform.position);
if (damageSource == 1)
infoText.text = "玩家" + nickname + "勇敢的用肉身挡住了导弹!";
else
infoText.text = "玩家" + nickname + "用生命帮你排除了一颗地雷!";
Invoke("EmptyInfoText", 3);
//关闭渲染器和碰撞体等
//ppc.SetInvisible();
ppc.CloseCollider();
ppc.canControll = false;
GetComponent<Animator>().SetBool("die", true);
Invoke("StopAnimationOnDieAnim", 0.5f);
}
#endregion
private void StopAnimationOnDieAnim()
{
//TODO:停止死亡动画保持死亡动作
}
private void EmptyInfoText()
{
infoText.text = "";
}
}
PlayerPropsController.cs
:
道具控制系统,“背包”、捡拾、道具使用。
其中的UseProp
方法是使用各种道具的方法、OnTriggerEnter
回调控制捡拾。
此代码默认3格背包满后再捡道具时顶替原来的第三格道具。
其中Cloak
方法是道具技能的隐身(下一篇文章的摄像机layer遮罩方式)。
public class PropsPlayerPropController : MonoBehaviour
{
enum RenderingMode
{
Opaque,
Transparent,
}
[Header("==游戏==")]
public LayerMask floorMask;
public PropsPlayerController ppc;
private Rigidbody thisRg;
private Animator animator;
public Text tipText;
[Header("==预制体==")]
public GameObject missilePrefab;
public GameObject landminePrefab; //放置的地雷预制体
[Header("==背包==")]
//第四位是闪现技能为1时表示冷却完毕,0时不可用
public PropsManager.PropsType[] itemBag = new PropsManager.PropsType[4];
public Transform bagItemsPanel;//背包的UI
public Sprite missileSprite;
public Sprite landMineSprite;
public Sprite cloakSprite;
public Sprite speedUpShoesSprite;
public Sprite noneSprite;
//[Header("==技能==")]
//冲刺技能
public float cdtime = 0;
public float cd = 7;
//加速的时间
private static float staySpeedUpTime = 2f;
private float currentSpeedUpTime = 0;
private bool isSpeedUp = false;
private float OldSpeed;//获取人物的初始速度
PhotonView photonView;
int noCameraLayer = 16;
#region unity
private void Start()
{
animator = this.GetComponent<Animator>();
photonView = GetComponent<PhotonView>();
thisRg = GetComponent<Rigidbody>();
if (!photonView.IsMine)
{
return;
}
for (int i = 0; i < itemBag.Length; i++)
{
itemBag[i] = PropsManager.PropsType.None;
}
GameObject canvas = GameObject.Find("UI");
bagItemsPanel = canvas.transform.GetChild(6).GetChild(1);
tipText = canvas.transform.GetChild(1).GetComponent<Text>();
OldSpeed = transform.gameObject.GetComponent<PropsPlayerController>().moveSpeed;
}
private void Update()
{
if(!photonView.IsMine)
{
return;
}
if(cdtime > 0)
{
cdtime -= Time.deltaTime;
//TODO:UI
}
if(!ppc.canControll)
{
return;
}
if (isSpeedUp) //判断是否加速中
{
if(currentSpeedUpTime < staySpeedUpTime)
{
currentSpeedUpTime += Time.deltaTime;
}
else //加速时间到了
{
isSpeedUp = false;
currentSpeedUpTime = 0;
UpandDownSpeed(OldSpeed);
}
}
Operate();
}
public void OnTriggerEnter(Collider other)//TODO:同步背包
{
//IsPickingPropUp
if (!other.gameObject.tag.Equals("DroppedProps"))
{
return;
}
if(other.gameObject.GetComponent<DroppedProps>() != null)
{
if (other.gameObject.GetComponent<DroppedProps>().propType == PropsManager.PropsType.HealthBottleProp)//血瓶不参与背包计算,且血瓶一个单独脚本执行逻辑
{
return;
}
}
//是否销毁(是否捡的起来)由玩家拥有者的背包判决。
int index = 0;
//***:主要用以通知主机,因为只有主机控制着销毁悬浮道具,可换为PhotonNetwork.IsMasterClient
if (!photonView.IsMine)
{
if (PhotonNetwork.IsMasterClient)
{
PhotonNetwork.Destroy(other.gameObject);
PropsManager.DecreaseCurrentCount();//场上数量-1
}
else
{
other.gameObject.SetActive(false);
}
}
else//自己
{
index = FindVacantIndex();
DroppedProps dp = other.gameObject.GetComponent<DroppedProps>();
DroppedRandomProps drp = other.gameObject.GetComponent<DroppedRandomProps>();
if (dp != null)
{
itemBag[index] = dp.propType; //添加背包里捡的道具的类型
//UI
bagItemsPanel.GetChild(index).GetComponent<Image>().sprite
= GetSpriteByPropType(dp.propType);
}
else if(drp != null)
{
itemBag[index] = drp.propType; //添加背包里捡的道具的类型
//UI
bagItemsPanel.GetChild(index).GetComponent<Image>().sprite
= GetSpriteByPropType(drp.propType);
}
other.gameObject.SetActive(false);
}
AudioManager.instance.PropsGamePickUp();
}
#endregion
#region key methods
public void Operate()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
//UseProp(PropsManager.PropsType.MissileProp);
//根据itemBag[0]的值执行
UseProp(itemBag[0]);
itemBag[0] = PropsManager.PropsType.None;//背包栏位置空
bagItemsPanel.GetChild(0).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
//UseProp(PropsManager.PropsType.CloakProp);
//UseProp(PropsManager.PropsType.SpeedUpProp);
UseProp(itemBag[1]);
itemBag[1] = PropsManager.PropsType.None;
bagItemsPanel.GetChild(1).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
//UseProp(PropsManager.PropsType.LandmineProp);
UseProp(itemBag[2]);
itemBag[2] = PropsManager.PropsType.None;
bagItemsPanel.GetChild(2).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
}
if (Input.GetKeyDown(KeyCode.E))
{
UseProp(PropsManager.PropsType.FlashSkill);
//itemBag[3] = 0;
}
}
private void UseProp(PropsManager.PropsType type)//TODO:E技能计时UI
{
switch (type)
{
case PropsManager.PropsType.None:
AudioManager.instance.NoProps();
break;
case PropsManager.PropsType.MissileProp:
//不能直接用这个刚体的位置和转向,刚体位置要高一点从臂膀发出,转向要指向地面
//鼠标
Vector3 playerToMouse = transform.forward;
Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit floorHit;
if (Physics.Raycast(camRay, out floorHit, 30f, floorMask))
{
//0.4比较精准触碰地板(考虑到导弹有体积可能会提前碰到地板)
playerToMouse = (floorHit.point + new Vector3(0, 0.4f, 0)) - (this.transform.position + Vector3.up);
}
//目前人物看向地板,应该可以直接用刚体朝向
photonView.RPC("Missile", RpcTarget.AllViaServer, thisRg.position + Vector3.up, playerToMouse);
break;
case PropsManager.PropsType.LandmineProp:
//先判断当前执行的动画是不是站立idel
if (animator.GetCurrentAnimatorStateInfo(0).IsName("IdleNormal02_AR_Anim"))//TODO: para:layerIndex:0???
{
Debug.Log("执行放置地雷的操作1");
DoAnimationWithEffect(2);
//创建地雷的方法
StartCoroutine(CreateLandMine(this.gameObject.transform.position));
}
else//不在空闲状态无法放置
{
tipText.text = "放置被打断";
Invoke("EmptyTipText", 1.5f);
}
break;
case PropsManager.PropsType.CloakProp:
photonView.RPC("Cloak", RpcTarget.AllViaServer);
break;
case PropsManager.PropsType.FlashSkill:
if(cdtime > 0)
{
AudioManager.instance.NoProps();
return;
}
//瞬移
AudioManager.instance.PropsGameFlash(transform.position);
Ray mouseRay = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
Vector3 direction = Vector3.zero;
if (Physics.Raycast(mouseRay, out hit, 30f, floorMask))
{
direction = hit.point - this.transform.position;
direction.y = 0f;
}
transform.DOBlendableMoveBy(direction.normalized * 4, 0.5f);
cdtime = cd;
break;
case PropsManager.PropsType.SpeedUpProp:
tipText.text = "速度提升";
Invoke("EmptyTipText", 1);
UpandDownSpeed(OldSpeed * 1.5f);
AudioManager.instance.PropGameSpeedUp(transform.position);
isSpeedUp = true;
break;
default:
break;
}
}
/// <summary>
/// type: 1击飞,2前倾放地雷。(起身动画由Animator自动跳转)
/// </summary>
/// <param name="animationType"></param>
public void DoAnimationWithEffect(int animationType)
{
if (!photonView.IsMine)
return;
switch (animationType)
{
case 1:
ppc.canControll = false;
animator.SetBool("fly", true);//被击飞,修改了string
Invoke("flyReset", 0.1f);//TODO:延时?其他方法?
Invoke("RestoreControl", 3);//TODO:要搭配击飞时间 + 起身时间
break;
case 2://地雷,失控2秒,后续可以实现取消放置
if (!ppc.canControll)
{
return;
}
ppc.canControll = false;
animator.SetBool("setLandMine", true);
Invoke("RestoreControl", 3);//两秒后恢复移动,放置地雷就两秒钟
Invoke("setLandMineFalse", 0.1f);
break;
}
}
private void flyReset()
{
animator.SetBool("fly", false);
}
private void RestoreControl()
{
ppc.canControll = true;
}
private void setLandMineFalse()
{
animator.SetBool("setLandMine", false);
}
#endregion
#region rpc 使用道具
[PunRPC]
public void Missile(Vector3 position, Vector3 playerToMouse, PhotonMessageInfo info)//传方向
{
//可以通过info中的延迟差,精确同步导弹
GameObject missile = Instantiate(missilePrefab, position, Quaternion.identity) as GameObject;
missile.GetComponent<PropMissile>().Initialize(photonView.Owner, playerToMouse);
AudioManager.instance.StartOffMissile(transform.position);
}
[PunRPC]
public void LandMineCreate(Vector3 pos)
{
GameObject landmine = Instantiate(landminePrefab, pos, Quaternion.identity);
}
[PunRPC]
public void Cloak()//隐身//TODO:累计隐身,需要改恢复状态逻辑
{
AudioManager.instance.Cloak(transform.position);
//他端隐藏3秒
//自己半透明3秒
if (photonView.IsMine)
{
SetMaterialRenderingMode(transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Transparent);
SetMaterialRenderingMode(transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Transparent);
Invoke("RestoreVisibleFromCloakWithMaterial", 3f);
}
else
{
//考虑到在隐身途中进入到了草里结束隐身时,所以与草不同要用不同的“隐身”逻辑以规避和草丛隐身的bug
transform.GetChild(0).gameObject.layer = noCameraLayer;
transform.GetChild(2).gameObject.layer = noCameraLayer;
transform.GetChild(5).gameObject.layer = noCameraLayer;
Invoke("RestoreLayer", 3f);
}
}
#endregion
#region private methods
//遍历背包返回满足的下标
/// <summary>
/// 背包满时返回2,默认顶替最后一格。可考虑满返回-1,但是用不到这个逻辑。
/// </summary>
private int FindVacantIndex()
{
for (int i = 0; i < itemBag.Length - 1; i++)//最后一格是技能不参与背包运算
{
if (itemBag[i] == PropsManager.PropsType.None)//i为空
{
return i;
}
}
return 2;
}
private void RestoreLayer()
{
transform.GetChild(0).gameObject.layer = 0;
transform.GetChild(2).gameObject.layer = 0;
transform.GetChild(5).gameObject.layer = 0;
}
private void RestoreVisibleFromCloakWithMaterial()
{
SetMaterialRenderingMode(transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Opaque);
SetMaterialRenderingMode(transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Opaque);
}
private void SetMaterialRenderingMode(Material material, RenderingMode renderingMode)
{
switch (renderingMode)
{
case RenderingMode.Opaque:
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.DisableKeyword("_ALPHATEST_ON");
material.DisableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = -1;
break;
case RenderingMode.Transparent:
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.DisableKeyword("_ALPHABLEND_ON");
material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = 3000;
break;
}
}
private void EmptyTipText()
{
tipText.text = "";
}
private void UpandDownSpeed(float s) //控制移动速度
{
transform.gameObject.GetComponent<PropsPlayerController>().moveSpeed = s;
}
//生成地雷
private IEnumerator CreateLandMine(Vector3 v) //主机生成,不知道咋弄。。。。
{
AudioManager.instance.SetLandmine(v);
yield return new WaitForSeconds(3); //等待3秒
v.y = 0.001f;
photonView.RPC("LandMineCreate", RpcTarget.AllViaServer, v);
}
private Sprite GetSpriteByPropType(PropsManager.PropsType propType)
{
switch (propType)
{
case PropsManager.PropsType.MissileProp:
return missileSprite;
case PropsManager.PropsType.LandmineProp:
return landMineSprite;
case PropsManager.PropsType.CloakProp:
return cloakSprite;
case PropsManager.PropsType.SpeedUpProp:
return speedUpShoesSprite;
case PropsManager.PropsType.None:
return noneSprite;
default:
return missileSprite;
}
}
#endregion
}
总结
关键代码就这么多。
因为这是项目最后开发的一个游戏,当时对pun多人化比较熟悉了。所以两个人用了四天左右的时间就开发出来了,而且本来量就不怎么大。
一共5+1个道具技能。和全局道具生成器。还有比较复杂的人物脚本。
配上音效后这个游戏趣味性还是挺高的。
《关于此游戏》
忽略美观程度,这种玩法本身具有很高的趣味性。
毕竟由于不同道具的组合使用,再结合地形特点,很容易被玩家研究出专属的“组合技”。
比如使用隐身、加速道具,再加上一个突进技能,到你脸前放一枚导弹…
或者开着隐身跑到另一个草丛藏起来…额太搞了…