这个流程主要实现以下功能:界面上显示玩家当前的总血量,当被其他玩家发射的子弹打中后,会减1格血,血量为0时判定为死亡,会重置玩家的位置到出生点,并且重设血量为最大血量。
实现这个逻辑,分了以下几个步骤。
1.维护玩家血量相关信息。
首先玩家血量要用图形化在界面上显示,首先需要在场景中建立一个GUITexture,这里起名为GUI_heart,来表示当前血量图片。为了控制血量的显示,为GUI_heartf附加了一个控制脚本,名为HeartControl.cs。这个脚本的内容如下:
01.using UnityEngine;
02.using System.Collections;
03.
04.public class HeartControl : MonoBehaviour {
05. public Texture2D[] heartImageArray;
06. private const int maxLives = 5;
07. private static int lives = maxLives;
08.
09. public static void DecreaseLive()
10. {
11. lives--;
12. if(lives==0)
13. {
14. lives=maxLives;
15. PlayerManager.selfPlayer.position = GameObject.Find("SpwanPlayer").transform.position;
16. }
17. }
18. // Use this for initialization
19. void Start () {
20.
21. }
22.
23. // Update is called once per frame
24. void Update () {
25. if(PlayerManager.selfPlayer!=null)
26. {
27. guiTexture.enabled = true;
28. guiTexture.texture = heartImageArray[lives-1];
29. }
30. else
31. {
32. guiTexture.enabled = false;
33. }
34.
35.
36. }
37.}
这里heartImageArray表示不同血量所对应的血量图片,血量用心型图标来显示,5格血就是5颗心。把这个数组声明为public,可以在编辑器中,设置这个数组的尺寸,以及每个入口点所对应的图片,十分方便。
maxLives就是最大血量有5格血,lives是当前人物的血量。
在每帧更新函数Update()中,首先检查当前玩家的变量是否为空,为空可能是因为是Server端,这种情况下就不需要显示血量,因此将图片设为false:guiTexture.enabled = false;
如果不为空,那么将根据当前血量的值,把对应数组中的图片赋值给这个guiTexture当前的texture,这样就实现图片随血量值的变化而变化。
这里PlayerManager.selfPlayer中的PlayerManager我自己建立的一个全局的静态类,维护了唯一的静态变量private static Transform _selfplayer,代表客户端所对应的玩家自己。这个类可以被所有脚本所访问,访问起来十分方便,实现如下:
01.using UnityEngine;
02.using System.Collections;
03.using System;
04.
05.public static class PlayerManager {
06.
07. private static Transform _selfplayer = null;
08. public static Transform selfPlayer
09. {
10. get
11. {
12. return _selfplayer;
13. }
14. set
15. {
16. _selfplayer = value;
17.
18. }
19. }
20.
21.}
后期随着逻辑的复杂,可以为这个脚本的玩家增加更多的数据来维护。
然后是DecreaseLive函数,这个函数可以被外部所调用,给玩家减血,调用后,自动扣除一滴血,为0时,正如规定的一样,重设玩家血量为最大血量,并且找到重生点的位置,将玩家位置重置。
2.控制子弹打中玩家的逻辑
实现大体流程是先给玩家的人物身上附加一个释放子弹的脚本,当客户端自己的玩家按键触发技能时,就调用一个网络上的RPC,告诉所有客户端和服务器在本地创建一个飞行的子弹。并且在这个人物脚本里添加一个响应人物碰撞的事件函数,然后检查是不是碰撞的对象是子弹,而且不是这个人物所发射的,如果检测成功,就调用HeartControl脚本的减血函数。
这个脚本名为Shoot.cs,实现如下:
001.using UnityEngine;
002.using System.Collections;
003.
004.public class Shoot : MonoBehaviour
005.{
006. public Texture skillTex1;
007. public Texture skillTex2;
008. public Transform bulletPrefab;
009. public Transform explosionEffect;
010. public AnimationClip attackAnim;
011.
012. // Use this for initialization
013. void Start ()
014. {
015.
016. }
017.
018. // Update is called once per frame
019. void Update ()
020. {
021. if (networkView.isMine && Input.GetKeyUp ("2")) {
022. ShootBullet ();
023.
024. }
025. if (networkView.isMine && Input.GetKeyUp ("3")) {
026. ExplosiveEffect();
027.
028. }
029.
030. }
031.
032. void OnGUI ()
033. {
034. if (networkView.isMine)
035. {
036. if(GUI.Button (new Rect ((float)(0.5*Screen.width-60), (float)(Screen.height-60), 60, 60), skillTex1))
037. {
038. ShootBullet();}
039.
040. if(GUI.Button(new Rect((float)0.5*Screen.width,(float)Screen.height-60,60,60),skillTex2))
041. {
042. ExplosiveEffect();
043. }
044. }
045.
046. }
047.
048. void OnCollisionEnter(Collision collisionInfo)
049. {
050.
051.
052. if(collisionInfo.gameObject.tag=="bullet" )
053. {
054. BulletScript bs = (BulletScript)collisionInfo.gameObject.GetComponent("BulletScript");
055. if(bs.Owner != gameObject)
056. {
057. print("OnCollisionEnter"+collisionInfo.gameObject.name);
058. Destroy(collisionInfo.gameObject);
059.
060.
061. if(!networkView.isMine)
062. return;
063. HeartControl.DecreaseLive();
064. }
065.
066.
067.
068. }
069. }
070.
071.
072.
073. void ExplosiveEffect()
074. {
075.
076. networkView.RPC("SpwanExplosion",RPCMode.All);
077. SendMessage("PlayAnimation_Attack");
078. }
079.
080. void ShootBullet()
081. {
082. networkView.RPC("SpawnBullet",RPCMode.All);
083. }
084.
085. [RPC]
086. void SpawnBullet ()
087. {
088.
089.
090. Vector3 forward = transform.TransformDirection (Vector3.forward);
091. Transform effectPoint = transform.Find ("effectPoint");
092. PrefabDepends preScript = (PrefabDepends)GameObject.Find("GameObject_GlobalController").GetComponent("PrefabDepends");
093. if (effectPoint) {
094.
095. Transform bullet = (Transform)Instantiate (preScript.bulletPrefab, effectPoint.position, Quaternion.identity);
096. bullet.rigidbody.AddForce (forward * 2000);
097. BulletScript bs = (BulletScript)bullet.GetComponent("BulletScript");
098. bs.Owner = gameObject;
099. }
100.
101. }
102.
103. [RPC]
104. void SpwanExplosion()
105. {
106. Vector3 forward = transform.TransformDirection (Vector3.forward);
107. Transform effectPoint = transform.Find ("effectPoint2");
108. if (effectPoint) {
109. //Object temp = Network.Instantiate (bulletPrefab, effectPoint.position, Quaternion.identity, 0);
110.
111. Transform explosion = (Transform)Instantiate (explosionEffect, effectPoint.position, Quaternion.identity);
112. //Network.Instantiate(bulletPrefab,transform.Find("effectPoint").position,Quaternion.identity,0);
113. }
114. }
115.
116.}
SpawnBullet就是调用的RPC call,在这里除了用子弹的prefab创建出子弹附加初始速度,还给子弹的脚本BulletScript的一个变量Owner赋值为脚本所附加到的玩家,这是为了判断这个子弹是哪个玩家发出用的。
碰撞检测的函数是OnCollisionEnter,要想触发这个函数,人物身上需要有一个Collider控件,而CharacterController是没有用的。所有的子弹都设为了名为"bullet"的tag,所以在这里检测,如果碰撞的对象是bullet,并且子弹上的脚本的变量Owner不是这个玩家自己,说明被别人的子弹击中,就先销毁本地创建出来的子弹,然后判断如果这个玩家是这个客户端所对应的,就进行减血。这个判断是必要的,因为这里的血量是全局静态的,只有一份代表自己,其他玩家被子弹攻击到了,不需要更新自己的血量。
人物的名称与血条的绘制方法很简单,但是我们需要解决的问题是如何在3D世界中寻找合适的坐标。因为3D世界中的人物是会移动的,它是在3D世界中移动,并不是在2D平面中移动,但是我们需要将3D的人物坐标换算成2D平面中的坐标,继而找到人物头顶在屏幕中的2D坐标最后使用GUI将名称与血条绘制出来。
首先学习本文的重点内容,如何将游戏世界中任意3D坐标转换成屏幕中的2D坐标。根据这个方法计算出的2D坐标屏幕左下角的点为0.0 ,屏幕右上角的坐标为1.1 所以真实的2D坐标还得通过Screen.height 与Screen.width计算一下才行。
1 |
Vector2 position = camera.WorldToScreenPoint (worldPosition); |
在Unity工程导入角色控制器组件,不知道角色控制器的朋友请阅读我之前的文章哈。创建一个Plane做为游戏的地面,然后利用角色控制器组件创建两个模型,一个做为主角,一个作为NPC,主角可以通过控制来移动从四周来观察NPC对象。由于地面的面积比较小移动主角时为了避免主角越界掉下去,我们做一个边界的物理层。物理层其实很简单,就是给平面四周放置四个平面在四周将平面包围着,给四周的四个平面绑定上Box Collider组件,这样主角就不会越界掉下去啦。因为没有给贴图所以效果上看不到这四个对象。哇咔咔~ 如下图所示,在场景是途中主角被四个平面包围这,即时它拼命的想往外条但是还是跳不出去,哈哈。
创建脚本NPC.cs 然后把脚本挂在NPC对象身上,在脚本中我们绘制主角的血条以及名称。
NPC.cs
001 |
using UnityEngine; |
002 |
using System.Collections; |
003 |
004 |
public class NPC : MonoBehaviour { |
005 |
006 |
//主摄像机对象 |
007 |
private Camera camera; |
008 |
//NPC名称 |
009 |
private string name = "我是雨松MOMO" ; |
010 |
011 |
//主角对象 |
012 |
GameObject hero; |
013 |
//NPC模型高度 |
014 |
float npcHeight; |
015 |
//红色血条贴图 |
016 |
public Texture2D blood_red; |
017 |
//黑色血条贴图 |
018 |
public Texture2D blood_black; |
019 |
//默认NPC血值 |
020 |
private int HP = 100; |
021 |
022 |
void Start () |
023 |
{ |
024 |
//根据Tag得到主角对象 |
025 |
hero = GameObject.FindGameObjectWithTag( "Player" ); |
026 |
//得到摄像机对象 |
027 |
camera = Camera.main; |
028 |
029 |
//注解1 |
030 |
//得到模型原始高度 |
031 |
float size_y = collider.bounds.size.y; |
032 |
//得到模型缩放比例 |
033 |
float scal_y = transform.localScale.y; |
034 |
//它们的乘积就是高度 |
035 |
npcHeight = (size_y *scal_y) ; |
036 |
037 |
} |
038 |
039 |
void Update () |
040 |
{ |
041 |
//保持NPC一直面朝主角 |
042 |
transform.LookAt(hero.transform); |
043 |
} |
044 |
045 |
void OnGUI() |
046 |
{ |
047 |
//得到NPC头顶在3D世界中的坐标 |
048 |
//默认NPC坐标点在脚底下,所以这里加上npcHeight它模型的高度即可 |
049 |
Vector3 worldPosition = new Vector3 (transform.position.x , transform.position.y + npcHeight,transform.position.z); |
050 |
//根据NPC头顶的3D坐标换算成它在2D屏幕中的坐标 |
051 |
Vector2 position = camera.WorldToScreenPoint (worldPosition); |
052 |
//得到真实NPC头顶的2D坐标 |
053 |
position = new Vector2 (position.x, Screen.height - position.y); |
054 |
//注解2 |
055 |
//计算出血条的宽高 |
056 |
Vector2 bloodSize = GUI.skin.label.CalcSize ( new GUIContent(blood_red)); |
057 |
058 |
//通过血值计算红色血条显示区域 |
059 |
int blood_width = blood_red.width * HP/100; |
060 |
//先绘制黑色血条 |
061 |
GUI.DrawTexture( new Rect(position.x - (bloodSize.x/2),position.y - bloodSize.y ,bloodSize.x,bloodSize.y),blood_black); |
062 |
//在绘制红色血条 |
063 |
GUI.DrawTexture( new Rect(position.x - (bloodSize.x/2),position.y - bloodSize.y ,blood_width,bloodSize.y),blood_red); |
064 |
065 |
//注解3 |
066 |
//计算NPC名称的宽高 |
067 |
Vector2 nameSize = GUI.skin.label.CalcSize ( new GUIContent(name)); |
068 |
//设置显示颜色为黄色 |
069 |
GUI.color = Color.yellow; |
070 |
//绘制NPC名称 |
071 |
GUI.Label( new Rect(position.x - (nameSize.x/2),position.y - nameSize.y - bloodSize.y ,nameSize.x,nameSize.y), name); |
072 |
073 |
} |
074 |
075 |
//下面是经典鼠标点击对象的事件,大家看一下就应该知道是什么意思啦。 |
076 |
void OnMouseDrag () |
077 |
{ |
078 |
Debug.Log( "鼠标拖动该模型区域时" ); |
079 |
} |
080 |
081 |
void OnMouseDown() |
082 |
{ |
083 |
Debug.Log( "鼠标按下时" ); |
084 |
085 |
if (HP >0) |
086 |
{ |
087 |
HP -=5 ; |
088 |
} |
089 |
090 |
} |
091 |
void OnMouseUp() |
092 |
{ |
093 |
Debug.Log( "鼠标抬起时" ); |
094 |
} |
095 |
096 |
void OnMouseEnter() |
097 |
{ |
098 |
Debug.Log( "鼠标进入该对象区域时" ); |
099 |
} |
100 |
void OnMouseExit() |
101 |
{ |
102 |
Debug.Log( "鼠标离开该模型区域时" ); |
103 |
} |
104 |
void OnMouseOver() |
105 |
{ |
106 |
Debug.Log( "鼠标停留在该对象区域时" ); |
107 |
} |
108 |
109 |
} |
注解1:通过collider.bounds.size 可以拿到模型对应三个轴向的高度,但是模型是可以缩放的,所以真实的模型高度应当是原始高度乘以缩放系数才行。 transform.localScale可以拿到模型对应三个轴向的缩放系数,因为这里我们需要模型的高度,所以忽略X轴与Z轴。
注解2:在这里我们计算血条的宽度,GUI.skin.label.Calcsize()这个方法是以默认的皮肤对象Label对象去参数对象的宽高。参数是new GUIContent(blood_Red)意思是拿红色血条的贴图的宽高,它将保存在返回的size中。最后以宽高将血条绘制在屏幕中,我们的血条采取两层。背景是黑色的,前面是红色的,当人物费血时红色血条减少。
注解3: 这里通过字符串来获取它整体的宽度与高度,因为NPC的名称是可变的,所以我们需要动态的获取整体的显示区域。同样是以GUI.skin.label对象去调用CalcSize。
如下图所示,当使用鼠标点击NPC对象时,NPC头顶的血条将开始发生减血。这个例子我使用OnGUI绘制当然大家也可以在Hierarchy 视图中的创建GUI Texture 或者GUI Text对象 来实现,不过原理都是这样的 大家可以试试 哇咔咔。
最后雨松MOMO希望和大家一起进步,一起学习,哇咔咔~