赞
踩
在游戏场景中,会有若干个敌人出生点,定时生成一些敌人,敌人会主动寻找并攻击主角。游戏目的就是生存下去,消灭僵尸敌人,获得更高的积分。
在游戏界面,包括主角的生命值,弹药数量,得分以及瞄准星。
游戏失败后,出现一个重新开始的按钮。
第一人称视角无法看到主角自己,在屏幕上只能看到一致端起来的M16枪械,键盘W、S、A、D键控住主角上下左右移动,鼠标移动来旋转视角。
敌人是一个护士模样的僵尸,具有自动寻路的功能,躲避障碍物,并攻击主角。
Lightmap
和Light Probe
表现静态和动态模型的光影效果。Mesh Collider
多边形碰撞组件。在开发中,模型通常复杂,会准备两组模型,一组质量较高用于显示,另一组用于物理碰撞。主角是看不见的,只能看到主角手里的枪械,尽管如此还是要为主角创建碰撞体,并控制其移动。
player
Character Controller
角色控制器,这样主角行走时就不会穿模Rigidbody
组件,取消重力模拟,选中Is Kinermaic使其不受物理推演影响。//Player.cs 完整代码 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { //组件 public Transform m_transform; CharacterController m_ch; //枪口transform Transform m_muzzlepoint; //射击时,射线能射到的碰撞层 public LayerMask m_layer; //射中目标后的粒子效果 public Transform m_fx; //射击音效 public AudioSource m_audio; public AudioClip m_shootClip; //声音 //射击间隔时间 float m_shootTimer = 0; //摄像机Transform Transform m_camTransform; Vector3 m_camRot; //相机旋转角度 float m_camHeight = 1.4f; //相机高度 //角色移动速度 float m_movSpeed = 3.0f; //重力 float m_grivity = 2.0f; //生命值 public int m_life = 5; // Start is called before the first frame update void Start() { //获取组件 m_transform = this.transform; m_ch = this.GetComponent<CharacterController>(); m_audio = this.GetComponent<AudioSource>(); //获取摄像机 m_camTransform = Camera.main.transform; //获取枪口 m_muzzlepoint = m_camTransform.transform.Find("M16/weapon/muzzlepoint").transform; //设置相机初始位置 Vector3 pos = m_transform.position; pos.y += m_camHeight; m_camTransform.position = pos; //设置摄像机的初始旋转角度 /* * rotation是Quaternion类型,可以随时改变旋转角度 * eulerAngles是Vector3类型,不能随时变化的,是一个定值 */ m_camTransform.rotation = m_transform.rotation; m_camRot = m_camTransform.eulerAngles; //锁定鼠标 Cursor.lockState = CursorLockMode.Locked; } // Update is called once per frame void Update() { //如果生命值为0,什么也不做 if (m_life <= 0) return; //更新射击间隔时间 m_shootTimer -= Time.deltaTime; //鼠标左键射击 if(Input.GetMouseButton(0) && m_shootTimer <= 0) { m_shootTimer = 0.1f; //射击音效 m_audio.PlayOneShot(m_shootClip); //减少弹药,更新UI GameManager.Instance.SetAmmo(1); //RaycastHit用来保存射线探测结果 RaycastHit info; //从muzzlepoint的位置,向摄像机面向的正方向射出一根射线 //射线只能与m_layer指定的层碰撞,Vector3.forward指(0,0,1)即z方向的单位向量 bool hit = Physics.Raycast(m_muzzlepoint.position, m_camTransform.TransformDirection(Vector3.forward), out info, 100, m_layer); //如果射中了Tag为enemy的物体 if (info.transform.tag.CompareTo("enemy") == 0) { Enemy enemy = info.transform.GetComponent<Enemy>(); //敌人减少生命 enemy.OnDamage(1); } //在射中的地方释放一个粒子效果 Instantiate(m_fx, info.point, info.transform.rotation); } Control(); } //控制角色移动以及视角变化 void Control() { float xm = 0, ym = 0, zm = 0; //获取鼠标移动距离 float rh = Input.GetAxis("Mouse X"); //右为正 float rv = Input.GetAxis("Mouse Y"); // 上为正 //旋转摄像机,z控制着倾斜程度,使保持不变 m_camRot.x -= rv; //上下旋转镜头是x控制,上为减 m_camRot.y += rh; //左右旋转镜头是y控制,左为减 m_camTransform.eulerAngles = m_camRot; //使主角面向方向与摄像机一致,即只有y轴跟着镜头转动,其他两个方向轴不动 Vector3 camrot = m_camTransform.eulerAngles; camrot.x = 0; camrot.z = 0; m_transform.eulerAngles = camrot; //重力运动 ym -= m_grivity * Time.deltaTime; //上下左右运动 if (Input.GetKey(KeyCode.W)) { zm += m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.S)) { zm -= m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.A)) { xm -= m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.D)) { xm += m_movSpeed * Time.deltaTime; } //移动,会根据角色的朝向,相对前后左右移动 m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm))); //使摄像机位置与主角一致 Vector3 pos = m_transform.position; pos.y += m_camHeight; m_camTransform.position = pos; } public void OnDamage(int damage) { m_life -= damage; //更新UI GameManager.Instance.setLife(m_life); //如果生命值为0,取消鼠标锁定 if(m_life <= 0) { Cursor.lockState = CursorLockMode.None; } } private void OnDrawGizmos() { Gizmos.DrawIcon(this.transform.position, "Spawn.tif"); } }
在Player.cs
脚本中添加部分代码,使摄像机伴随主角移动
具体实现部分代码:
void Control() { float xm = 0, ym = 0, zm = 0; //获取鼠标移动距离 float rh = Input.GetAxis("Mouse X"); //右为正 float rv = Input.GetAxis("Mouse Y"); // 上为正 //旋转摄像机,z控制着倾斜程度,使保持不变 m_camRot.x -= rv; //上下旋转镜头是x控制,上为减 m_camRot.y += rh; //左右旋转镜头是y控制,左为减 m_camTransform.eulerAngles = m_camRot; //使主角面向方向与摄像机一致,即只有y轴跟着镜头转动,其他两个方向轴不动 Vector3 camrot = m_camTransform.eulerAngles; camrot.x = 0; camrot.z = 0; m_transform.eulerAngles = camrot; //重力运动 ym -= m_grivity * Time.deltaTime; //上下左右运动 if (Input.GetKey(KeyCode.W)) { zm += m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.S)) { zm -= m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.A)) { xm -= m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.D)) { xm += m_movSpeed * Time.deltaTime; } //移动,会根据角色的朝向,相对前后左右移动 m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm))); //使摄像机位置与主角一致 Vector3 pos = m_transform.position; pos.y += m_camHeight; m_camTransform.position = pos; }
把武器绑定到摄像机上,使其能够伴随主角移动
Clipping Plane/Near
设为0.1,使其可以看到更近处的物体Unity提供了非常实用的寻路功能,其寻路系统分为两个部分,一部分是对场景进行设置,使其满足寻路算法的需求,另一部分是设置寻路者。
//Enemy.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { Transform m_transform; Player m_player; UnityEngine.AI.NavMeshAgent m_agent; //寻路组件 float m_movSpeed = 0.5f; //敌人移动速度 float m_rotSpeed = 120; //角色旋转速度 float m_timer = 2; //计时器 int m_life = 5; //生命值 //出生点 protected EnemySpawn m_spawn; //初始化 public void Init(EnemySpawn spawn) { m_spawn = spawn; m_spawn.m_enemyCount++; } Animator m_ani; //动画组件 // Start is called before the first frame update void Start() { //获取组件 m_transform = this.transform; m_ani = GetComponent<Animator>(); //获取动画组件 //获取主角类实例 m_player = GameObject.FindGameObjectWithTag("player").GetComponent<Player>(); //获取寻路组件 m_agent = GetComponent<UnityEngine.AI.NavMeshAgent>(); //设置寻路目标 m_agent.SetDestination(m_player.m_transform.position); } // Update is called once per frame void Update() { // 如果主角生命为0,什么也不做 if (m_player.m_life <= 0) return; // 获取当前动画状态 AnimatorStateInfo stateInfo = m_ani.GetCurrentAnimatorStateInfo(0); // 如果处于待机状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.idle") && !m_ani.IsInTransition(0)) { m_ani.SetBool("idle", false); // 待机一定时间 m_timer -= Time.deltaTime; if (m_timer > 0) return; // 如果距离主角小于1.5米,进入攻击动画状态 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) < 1.5f) { m_ani.SetBool("attack", true); } else { // 重置定时器 m_timer = 1; // 设置寻路目标点 m_agent.SetDestination(m_player.m_transform.position); // 进入跑步动画状态 m_ani.SetBool("run", true); } } // 如果处于跑步状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.run") && !m_ani.IsInTransition(0)) { m_ani.SetBool("run", false); // 每隔1秒重新定位主角的位置 m_timer -= Time.deltaTime; if (m_timer < 0) { m_agent.SetDestination(m_player.m_transform.position); m_timer = 1; } // 追向主角 MoveTo(); // 如果距离主角小于1.5米,向主角攻击 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) <= 1.5f) { //停止寻路 m_agent.ResetPath(); m_ani.SetBool("attack", true); } } // 如果处于攻击状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.attack") && !m_ani.IsInTransition(0)) { // 面向主角 RotateTo(); m_ani.SetBool("attack", false); // 如果攻击动画播完,重新进入待机状态 if (stateInfo.normalizedTime >= 1.0f) { m_ani.SetBool("idle", true); // 重置计时器 m_timer = 2; //更新主角生命 m_player.OnDamage(1); } } if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.death") && !m_ani.IsInTransition(0)) { //当播放完死亡动画 if (stateInfo.normalizedTime >= 1.0f) { OnDeath(); } } } //寻路移动 void MoveTo() { float speed = m_movSpeed * Time.deltaTime; //TransformDirection从自身坐标到世界坐标变换方向 m_agent.Move(m_transform.TransformDirection(new Vector3(0, 0, speed))); } //始终转向主角位置 void RotateTo() { //当前角度 Vector3 oldangle = m_transform.eulerAngles; //获得面向主角的角度 m_transform.LookAt(m_player.m_transform); float target = m_transform.eulerAngles.y; //转向主角 float speed = m_rotSpeed * Time.deltaTime; float angle = Mathf.MoveTowardsAngle(oldangle.y, target, speed); //计算出当前角度转向目标角度的旋转角度 m_transform.eulerAngles = new Vector3(0, angle, 0); } public void OnDamage(int damage) { // 伤害 m_life -= damage; if (m_life <= 0) { m_ani.SetBool("death", true); } } //当被销毁时 public void OnDeath() { //更新敌人数量 m_spawn.m_enemyCount--; //加分 GameManager.Instance.SetScore(100); //销毁 Destroy(this.gameObject); } }
为敌人的不同动作设置不同的动画
在场景中选中敌人,默认它有一个Animator组件,在Controller中设置好一个Animator Controller,取消选择Apply Root Motion选项,强迫使其受脚本控制。
【window】-【Animation】-【Animator】打开Animator窗口,添加动画过渡。点击状态方格,为其添加动画;点击箭头为其添加过渡条件。
点击【parameter】,为几个状态添加参数。注意:不要把旁边的选项勾上
设置动画过渡条件
修改敌人的脚本,在不同状态时使敌人的行为也发生改变。
RotateTo
函数,使敌人始终转到面向主角的角度//始终转向主角位置 void RotateTo() { //当前角度 Vector3 oldangle = m_transform.eulerAngles; //获得面向主角的角度 m_transform.LookAt(m_player.m_transform); float target = m_transform.eulerAngles.y; //转向主角 float speed = m_rotSpeed * Time.deltaTime; float angle = Mathf.MoveTowardsAngle(oldangle.y, target, speed); //计算出当前角度转向目标角度的旋转角度 m_transform.eulerAngles = new Vector3(0, angle, 0); }
// 获取当前动画状态 AnimatorStateInfo stateInfo = m_ani.GetCurrentAnimatorStateInfo(0); // 如果处于待机状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.idle") && !m_ani.IsInTransition(0)) { m_ani.SetBool("idle", false); // 待机一定时间 m_timer -= Time.deltaTime; if (m_timer > 0) return; // 如果距离主角小于1.5米,进入攻击动画状态 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) < 1.5f) { m_ani.SetBool("attack", true); } else { // 重置定时器 m_timer = 1; // 设置寻路目标点 m_agent.SetDestination(m_player.m_transform.position); // 进入跑步动画状态 m_ani.SetBool("run", true); } } // 如果处于跑步状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.run") && !m_ani.IsInTransition(0)) { m_ani.SetBool("run", false); // 每隔1秒重新定位主角的位置 m_timer -= Time.deltaTime; if (m_timer < 0) { m_agent.SetDestination(m_player.m_transform.position); m_timer = 1; } // 追向主角 MoveTo(); // 如果距离主角小于1.5米,向主角攻击 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) <= 1.5f) { //停止寻路 m_agent.ResetPath(); m_ani.SetBool("attack", true); } } // 如果处于攻击状态 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.attack") && !m_ani.IsInTransition(0)) { // 面向主角 RotateTo(); m_ani.SetBool("attack", false); // 如果攻击动画播完,重新进入待机状态 if (stateInfo.normalizedTime >= 1.0f) { m_ani.SetBool("idle", true); // 重置计时器 m_timer = 2; //更新主角生命 m_player.OnDamage(1); } } if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.death") && !m_ani.IsInTransition(0)) { //当播放完死亡动画 if (stateInfo.normalizedTime >= 1.0f) { OnDeath(); } }
使用2D贴图的方式展示UI效果,比使用OnGUI
更节省资源。
//GameManager.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance = null; //游戏得分 int m_score = 0; //游戏最高分 static int m_hiscore = 0; //弹药数量 int m_ammo = 100; //游戏主角 Player m_player; //UI文字 UnityEngine.UI.Text txt_ammo; UnityEngine.UI.Text txt_hiscore; UnityEngine.UI.Text txt_life; UnityEngine.UI.Text txt_score; // Start is called before the first frame update void Start() { Instance = this; //获取主角 m_player = GameObject.FindGameObjectWithTag("player").GetComponent<Player>(); //获得设置的UI文字 txt_ammo = this.transform.Find("Canvas/txt_ammo").GetComponent<UnityEngine.UI.Text>(); txt_hiscore = this.transform.Find("Canvas/txt_hiscore").GetComponent<UnityEngine.UI.Text>(); txt_life = this.transform.Find("Canvas/txt_life").GetComponent<UnityEngine.UI.Text>(); txt_score = this.transform.Find("Canvas/txt_score").GetComponent<UnityEngine.UI.Text>(); } //更新分数 public void SetScore(int score) { m_score += score; if(m_score > m_hiscore) { m_hiscore = m_score; } txt_score.text = "Score " + m_score; txt_hiscore.text = "High Score " + m_hiscore; } //更新弹药 public void SetAmmo(int ammo) { m_ammo -= ammo; //如果弹药为负数,重新填弹 if(m_ammo <= 0) { m_ammo = 100 - m_ammo; } txt_ammo.text = m_ammo.ToString() + "/100"; } //更新生命 public void setLife(int life) { txt_life.text = life.ToString(); } private void OnGUI() { if(m_player.m_life <= 0) { //显示游戏结束 GUI.skin.label.alignment = TextAnchor.MiddleCenter; GUI.skin.label.fontSize = 40; GUI.Label(new Rect(0, 0, Screen.width, Screen.height), "Game Over"); //显示游戏重新开始 GUI.skin.label.fontSize = 30; if (GUI.Button(new Rect(Screen.width * 0.5f - 150, Screen.height * 0.75f, 300, 40), "Try Again")) { //Application.LoadLevel(Application.loadedLevelName); UnityEngine.SceneManagement.SceneManager.LoadScene("Demo"); } } } // Update is called once per frame void Update() { } }
OnDamage
函数,用于减少主角生命,并更新UI//AutoDestroy.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class AutoDestroy : MonoBehaviour { public float m_timer = 1.0f; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { m_timer -= Time.deltaTime; if (m_timer <= 0) Destroy(this.gameObject); } }
为了能控制敌人数量,每个出生点都需要清楚生成了多少敌人,达到最大值时停止产生敌人,敌人被消灭时提示产生新的敌人。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemySpawn : MonoBehaviour { public Transform m_enemy; //放入敌人预设 public int m_enemyCount = 0; //敌人的数量 public int m_maxEnemy = 3;//敌人的最大生成数量 public float m_timer = 0; //敌人生成间隔时间 protected Transform m_transform; // Start is called before the first frame update void Start() { m_transform = this.transform; } // Update is called once per frame void Update() { //如果生成敌人数量达到最大值,停止生成敌人 if (m_enemyCount >= m_maxEnemy) return; //每隔一段时间,生成敌人 m_timer -= Time.deltaTime; if(m_timer <= 0) { m_timer = 5+Random.value * 10.0f; Transform obj = (Transform)Instantiate(m_enemy, m_transform.position, Quaternion.identity); //获得敌人脚本 Enemy enemy = obj.GetComponent<Enemy>(); enemy.Init(this); } } private void OnDrawGizmos() { Gizmos.DrawIcon(this.transform.position, "item.png",true); } }
创建一个新的【Camera】,放在地图的正上方,对其进行设置
在敌人和主角的子项中分别创建不同颜色的球体对象,命名为dummy,作为主角和敌人的“替代品”,注意要把球体的【Sphere Colider】去掉
创建一个新的Layer,命名为dummy,并设置球体对象的Layer为dummy
选择主摄像机,使其不显示dummy层
选择新的摄像机,使其只显示dummy层和场景level层
显示小地图:
6.1首先创建一个新的【Render Texture】,命名为camera;
6.2在GameManager中添加一个【Image】,并用camera为其赋值,调整大小;
6.3为新的相机的【Target Texture】赋值
代码及资料
有网友反映尝试将代码跑起来时,会出现以下问题,问题截图来源于网友:
错误提示:nullreference exception。object reference not set to an instance of an object
鉴于有多个网友私信我这个问题,我将之前回复的内容统一贴一下(由于项目有点久,很多细节我自己也记不清懒得回头一点点分析,只把之前帮网友解决问题的回复贴一下):
大致说一下各个script.cs的执行流程,首先是Enemy spawn空对象(不可见)一开始就存在场景中,它执行update函数,通过Instantiate函数产生enemy对象(可见),然后执行enemy对象的init函数初始化,并将当前enemySpawn对象当做参数传入,这样enemy对象中的protect属性m_spawn就有值所以不会有空指针异常。所以反过来推测,产生空指针异常是这个m_spawn为空导致的,所以是不是你自己手动生成了一个Enemy对象,然后开枪打死他所以才会报错。正确的流程应该是敌人是通过spawn产生,这样打死他才能正确的把对应的spawn对象的enemyCount值-1
简单总结以下:教程中是一点点由浅入深,开始教的大家如何在场景中手动创建一个prefab敌人,后来深入后是自动创建prefab敌人,并且有了后续消灭敌人的一系列流程。而一开始手动创建的prefab敌人是没有后续的,所以教程讲解到自动生成敌人时,要把之前教程中场景中手动创建的敌人prefab删除,就解决了这个问题。之前写教程的时候是完成之后回过头来写的,所以这一步貌似忽略了。
另外,本人也是看《unity 3D手机游戏开发 金玺曾》学习一边做的,书中很多细节和新版的unity有些不一样,大家可以对照着看。书和书附赠的代码链接我也一并放出来:
链接:https://pan.baidu.com/s/1c6FviWaYhnWdjjSWQOYtUg?pwd=1234
提取码:1234
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。