本文是学习官方教程SURVIVAL SHOOTER TUTORIAL的笔记;
主要目的是用于记录一些关键的代码和步骤;
如果感兴趣,推荐还是观看官方的教程:教程链接
预览
1. 控制角色移动
创建地板用于射线捕捉,把Layer设定为Floor
新建一个PlayerMovement脚本,绑定在角色上,并且添加以上属性
public float speed = 6f;
Vector3 movement;
Animator anim;
Rigidbody playerRigidbody;
public int floorMask;
float camerRayLength = 100f;
void Awake (){
floorMask = LayerMask.GetMask ("Floor"); //绑定LayerMask
anim = GetComponent<Animator> ();
playerRigidbody = GetComponent<Rigidbody> ();
}
射线检测是否和地面碰撞,并且让角色旋转
void Turing() {
//捕捉主摄像机和鼠标交集的射线
Ray camRay = Camera.main.ScreenPointToRay (Input.mousePosition);
RaycastHit floorHit;
//射线, out 射线点, 长度, 层级,以上几个关键属性如果产生交集就让角色旋转
if (Physics.Raycast (camRay, out floorHit, camerRayLength, floorMask)) {
Vector3 playerToMouse = floorHit.point - transform.position;
playerToMouse.y = 0f;
Quaternion newRotation = Quaternion.LookRotation (playerToMouse);
playerRigidbody.MoveRotation (newRotation);
}
}
捕捉输入的h和v,移动角色
void Move(float h, float v) {
movement.Set (h, 0, v);
movement = movement.normalized * speed * Time.deltaTime;
playerRigidbody.MovePosition (transform.position + movement);
}
如果角色移动就播放移动动画
//如果输入的上下左右不等于0,则让动画状态机的IsWalking属性变为true,具体的动画切换在角色绑定的动画状态机中
void Animating (float h, float v){
bool walking = (h != 0f || v != 0f);
anim.SetBool ("IsWalking", walking);
}
FiexdUpdate输入Input,并且统一调用以上几个方法
void FixedUpdate(){
//具体的单词参考Edit->ProjectSetting->Input面板
float h = Input.GetAxisRaw ("Horizontal");
float v = Input.GetAxisRaw ("Vertical");
Move (h, v); //移动角色
Animating (h, v); //判断是否播放移动动画
Turing (); //旋转角色
}
ok 运行游戏试试看,此时玩家可以使用键盘移动了,并且会面朝鼠标指向的方向.
2. 控制摄像机跟随玩家
新建一个CameraLookAt脚本,代码如下
public class CameraLookAtPlayer : MonoBehaviour {
public Transform target; //用于编辑器中绑定玩家
public float smoothing = 5f; //用于计算顺滑度
Vector3 offset;
void Start() {
//首先初始化的时候保存相机和玩家的相对位置
offset = transform.position - target.position;
}
//这里不要使用FixedUpdate, 移动端屏幕会有视觉卡顿
void Update () {
//计算出相机跟随的位置
Vector3 targetCamPos = target.position + offset;
//设置相机的位置,这里用到了Vector3.Lerp,是一个差值计算,使得移动更柔和.但是会略微消耗计算量
//由于主摄像机只有1个,所以可以忽略这个计算量的消耗
transform.position = Vector3.Lerp (transform.position, targetCamPos, smoothing * Time.deltaTime);
}
}
在项目中设定好摄像机和玩家的距离
然后在把CamerLookAt脚本绑定在主摄像机上,并且把玩家设定到target属性中
相机的设置
ok,运行一下游戏,此时摄像机会跟随玩家移动,并且有一个缓慢加速的过程
3.创建一个敌人,自动寻路跟随玩家
首先添加Navigation烘焙
- 首先把场景中需要烘焙的物件设置为static, 一些障碍物体要添加碰撞体
- 点击Window->Navigation面板,设置烘焙属性并且烘焙
创建一个怪物并且基础设置
- 设置Rigidbody
- 添加一个胶囊碰撞体,CapsuleCollider组件,用于寻找玩家做碰撞
- 添加一个球碰撞体,SphereCollider组件,用于以后攻击玩家做碰撞
- 添加一个寻路组件,NavMeshAgent,用于配合Navigation寻路
- 给怪物设置一个动画控制机AminatorController,用于切换动画.
给添加怪物寻路代码
public class ZombunnyMovement_ym : MonoBehaviour {
Transform player;
NavMeshAgent nav;
void Awake(){
//因为怪物是生成器生成的,所以player需要在生成的时候遍历一下场景,找到玩家
player = GameObject.FindGameObjectWithTag ("Player").transform;
//读出导航组件
nav = GetComponent<NavMeshAgent> ();
}
void Update() {
//导航到目的地:玩家的坐标,这里注意,一定是transform.position,而不是transform
nav.SetDestination (player.position);
}
}
ok,此时运行游戏,怪物会一直朝着玩家移动
4.设置血条UI
首先设置UI如下图效果
- 在场景中创建一个Canvas
- 里面创建一个空物体用来存放HealthUI
-
创建一个Slider,删除手柄,只要进度条,用来显示血量
Hierarchy中的层级
如图,左下角有一个红心,和一个Slider,Slider删掉了手柄
声明一下,这种UI不是我做的......
5.给玩家添加生命控制的脚本,和给敌人添加攻击的脚本
首先是添加控制玩家生命的代码
新建一个PlayerHealth脚本
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class PlayerHealth_ym : MonoBehaviour {
public int startingHealth = 100;
public int currentHealth; //当前血量
public Slider healthSlider; //用来存放Slider,收到伤害修改它的值
public Image damageImage; //之前设置的收到伤害覆盖全屏红色闪烁的图片
public AudioClip deathClip; //用来存放死亡的声音
public float flashSpeed = 5f;
public Color flashColor = new Color (1f, 0f, 0f, 0.1f);
Animator anim;
AudioSource playAudio;
PlayerMovement_ym playMovement; //用来死亡的时候取消移动组件,防止玩家移动
bool isDead;
bool damaged;
void Awake () {
anim = GetComponent<Animator> ();
playAudio = GetComponent<AudioSource> ();
playMovement = GetComponent<PlayerMovement_ym> ();
currentHealth = startingHealth;
}
// Update is called once per frame
void Update () {
//如果受到伤害,就改变图片颜色,否则,Lerp过渡颜色到空颜色
if(damaged) {
damageImage.color = flashColor;
} else {
damageImage.color = Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime);
}
damaged = false;
}
//public方法,用于怪物攻击玩家时调用
public void TakeDamage(int amount) {
damaged = true;
currentHealth -= amount;
healthSlider.value = currentHealth;
playAudio.Play ();
if(currentHealth <= 0 && !isDead) {
Death ();
}
}
//死亡时播放死亡声音,设置死亡动画,并且关闭玩家移动方法
void Death(){
isDead = true;
anim.SetTrigger ("Die");
playAudio.clip = deathClip;
playAudio.Play ();
playMovement.enabled = false;
}
}
接着给脚本的public属性绑定物件,如图:
接下来是给怪物添加攻击脚本
新建一个Empty_Attack脚本:
using UnityEngine;
using System.Collections;
public class Empty_Attack_ym : MonoBehaviour {
public float timeBetweenAttacks = 0.5f; //攻击间隔
public int attackDamage = 10; //攻击力
public bool playerInRange; //玩家是否在攻击范围内
Animator anim;
GameObject player;
PlayerHealth_ym playerHealth;
float timer;
void Awake(){
player = GameObject.FindGameObjectWithTag ("Player");
playerHealth = player.GetComponent<PlayerHealth_ym> ();
anim = GetComponent<Animator> ();
}
//当碰撞体进入
void OnTriggerEnter (Collider other){
if(other.gameObject == player) {
playerInRange = true;
}
}
//当碰撞体离开
void OnTriggerExit (Collider other) {
if(other.gameObject == player) {
playerInRange = false;
}
}
void Update(){
timer += Time.deltaTime;
//当攻击正在范围内,并且timer的值大于攻击间隔,进行攻击
if(timer >= timeBetweenAttacks && playerInRange) {
Attack ();
}
//当玩家当前生命小于0, 怪物的动画状态机切换为玩家已死的状态(此时播放开心的动画);
if(playerHealth.currentHealth <= 0) {
anim.SetTrigger ("PlayerDie");
}
}
void Attack(){
timer = 0f;
if(playerHealth.currentHealth > 0) {
//调用玩家生命控制器的被攻击的方法,传入攻击伤害值
playerHealth.TakeDamage (attackDamage);
}
}
}
ok,运行一下,玩家会被怪物打死...
6. 给玩家添加攻击动作,和怪物生命脚本
首先配置资源
- 把玩家射击的粒子预设组件拷贝到玩家的枪上
- 给玩家的枪添加一个Line Renderer组件
- 设置Parameters的StartWidth和EndWidth为0.07
- 设置Materials->Element0的材质为LineRenderMaterial
- 给玩家的枪添加一个Light组件,并且设置好亮度角度, 初始取消它,等待攻击的时候再临时激活
- 把怪物受到伤害的粒子组件拷贝到怪物身上
- 把怪物的Layer设置为Shootable,一会射线检测的LayerMask用得到
开始添加玩家攻击脚本PlayerShooting
代码都有注释, 就不需要额外说明了
using UnityEngine;
using System.Collections;
public class PlayerShooting_ym : MonoBehaviour {
public int damagePerShot = 20; //伤害
public float timeBetweenBullets = 0.15f; //攻击间隔
public float range = 100f; //射线最大距离
float timer; //用于攻击间隔计时
Ray shootRay; //子弹的射线
RaycastHit shootHit;
int shootableMask; //用于存放LayerMask
ParticleSystem gunParticles;
LineRenderer gunLine; //射线的渲染
AudioSource gunAudio;
Light gunLight; //枪上的灯光
float effectsDisplayTime = 0.2f; //粒子时长比例
void Awake() {
shootableMask = LayerMask.GetMask ("Shootable");
gunParticles = GetComponent<ParticleSystem> ();
gunLine = GetComponent<LineRenderer> ();
gunAudio = GetComponent<AudioSource> ();
gunLight = GetComponent<Light> ();
}
void Start () {
}
// Update is called once per frame
void Update () {
timer += Time.deltaTime;
//当点击Fire1键时,并且攻击间隔达到预设时,可以攻击
if(Input.GetButton("Fire1") && timer >= timeBetweenBullets) {
Shoot ();
}
//当计时器大于枪的粒子时间时,取消射击的样式, 这里射击样式的时间是攻击间隔 * 粒子时长比例
if(timer >= timeBetweenBullets * effectsDisplayTime) {
DisableEffects ();
}
}
// Shoot方法是关键
void Shoot() {
timer = 0f;
//激活声音,灯光
gunAudio.Play ();
gunLight.enabled = true;
//停止之前的射击粒子,重新播放
gunParticles.Stop ();
gunParticles.Play ();
//打开LineRenderer组件, 设置0点为gameobject默认位置
gunLine.enabled = true;
gunLine.SetPosition (0, transform.position);
//设置射线的位置
shootRay.origin = transform.position;
//设置射线的方向,正前方
shootRay.direction = transform.forward;
//用于捕捉射线,然后根据射线的射击点进行做一些事情
//物理.射线捕捉(射线, 得到的点shootHit, 最大距离, 进行计算的Layer)
if(Physics.Raycast(shootRay, out shootHit, range, shootableMask)) {
//得到怪物的生命组件
ZombunnyHealth_ym enemyHealth = shootHit.collider.GetComponent<ZombunnyHealth_ym> ();
//如果怪物的生命组件不为空, 调用这个组件的TakeDamage方法进行扣血
if(enemyHealth != null) {
enemyHealth.TakeDamage (damagePerShot, shootHit.point);
}
//设置LineRenderer的第二个位置,为shootHit.point, 两点确定一条直线
gunLine.SetPosition (1, shootHit.point);
}
else {
//如果没有捕捉到射击点
//设置LineRenderer的第二个位置为, 射线初始位置 + 射线方向(0, 0, 1) * 最大距离
gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range);
}
}
void DisableEffects(){
//取消LineRenderer组件和灯光组件
gunLine.enabled = false;
gunLight.enabled = false;
}
}
给怪物添加生命组件ZombunnyHealth
public int startingHealth = 100; //初始生命
public int currentHealth; //当前生命
public float sinkSpeed = 2.5f; //下沉速度
public int scoreValue = 10; //此怪物杀死计多少分
public AudioClip deathClip; //保存死亡时播放的声音
Animator anim;
AudioSource enemyAudio;
ParticleSystem hitParticles;
CapsuleCollider capsuleCollider; //capsulecollider是用来做移动碰撞捕捉
bool isDead;
bool isSinking;
void Awake(){
anim = GetComponent<Animator> ();
enemyAudio = GetComponent<AudioSource> ();
hitParticles = GetComponentInChildren<ParticleSystem> ();
capsuleCollider = GetComponent<CapsuleCollider> ();
currentHealth = startingHealth;
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
//如果可以下沉, 设定下沉的动画
//transform.Translate是位置变化,必须放在循环中,才能达到像动画一样的效果
if(isSinking){
//位置.变化((0, -1, 0) * 下沉速度)
transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
}
}
public void TakeDamage(int amount, Vector3 hitPoint) {
if(isDead) {
return;
}
//播放挨打的声音
enemyAudio.Play ();
//设置粒子的位置等于攻击点的位置
hitParticles.transform.position = hitPoint;
//播放粒子
hitParticles.Play ();
//扣血
currentHealth -= amount;
if(currentHealth <= 0) {
//调用死亡方法
Death ();
}
}
void Death() {
isDead = true;
//把碰撞体设置为isTrigger
capsuleCollider.isTrigger = true;
//设置动画控制器的触发"Dead"
anim.SetTrigger ("Dead");
//切换声音片为死亡声音
enemyAudio.clip = deathClip;
//播放声音
enemyAudio.Play ();
}
//这个是公开方法, 实在模型动画里的event调用的,当动画播放到某一个时间时,会调用这个函数
public void StartSinking(){
//取消寻路组件
GetComponent<NavMeshAgent> ().enabled = false;
//取消刚体组件
GetComponent<Rigidbody> ().isKinematic = true;
//设置可以下沉, 一会在Updata里面就会调用下沉动画
isSinking = true;
//添加修改静态变量,然后更新UI的分数
SourceValue_ym.score += scoreValue;
//两秒之后销毁本物体
Destroy (gameObject, 2f);
}
}
给场景添加计分Text
-
在场景中间添加一个Text,如图:
- 给Text添加一个SourceValue组件,用于改变Text的文字
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class SourceValue_ym : MonoBehaviour {
public static int score;
Text text;
void Awake () {
text = GetComponent<Text> ();
score = 0;
}
// Update is called once per frame
void Update () {
//之前在ZombunnyHealth组件中已经添加了怪物死亡修改score的方法
text.text = "Score: " + score;
}
}
ok,现在运行一下游戏, 玩家可以射击攻击怪物了,并且怪物会被玩家打死, 而且有计分
一具尸体...
6. 创建怪物生成器
首先创建两个新怪物,引用之前的逻辑
第一个怪物ZomBear:
- 把ZomBear的模型拖进Scene中
- 把Zombunny的所有组件拷贝到ZomBear上: 拷贝所有组件的方法扩展链接
- 因为两个动画状态机和Avatar(化生)都是一样的,所以动画状态机可以使用同一个
第二个怪物Hellephant:
- 把Hellephant的模型拖进Scene中
- 把Zombunny的所有组件拷贝到Hellephant上
- 因为两个动画的Avatar不一样, 所以播放的动画不能重用, 但是动画逻辑可以重用
- 创建一个Animator Override Controller(动画覆盖控制器)
- 动画控制器选ZombunnyAC, Original中的动画片段选用Hellephant自己的动画
-
这样就覆盖动画片段, 并且使用原有的动画逻辑了
Animator Override Controller 例子
修改一下两个新怪物的属性,需要修改的内容有:
- 移动速度
- 生命值
- 攻击力, 攻击间隔
- 收到伤害声音, 死亡声音
接下来创建怪物生成器的代码
创建EnemyManager组件, 一会用来生成怪物
using UnityEngine;
using System.Collections;
public class EnemyManager_ym : MonoBehaviour {
public PlayerHealth_ym playerHealth; //玩家的血量
public GameObject enemy; //要创建的怪物
public float spawnTime = 3f; //创建间隔时间
public Transform[] spawnPoints; //创建位置
// Use this for initialization
void Start () {
//循环调用("方法名", 初始等待时间, 循环间隔时间)
InvokeRepeating ("Spawn", spawnTime * 0.7f, spawnTime);
}
void Spawn() {
if(playerHealth.currentHealth <= 0) {
return;
}
//随机得到数组范围内的一个整数
int spawnPointIndex = Random.Range (0, spawnPoints.Length);
//实例化一个物件(物件, 位置, 旋转角度);
Instantiate (enemy, spawnPoints [spawnPointIndex].position, spawnPoints [spawnPointIndex].rotation);
}
}
- 在场景中创建一个空物体, 把坐标还原成(0, 0, 0), 取名为EnemyManager
- 在EnemyManager中创建三个空物体, 并且拖动到场景不同的位置中, 用于设定3种怪物的生成位置
- 在EnemyManager中绑定三个EnemyManager组件, 用于创建三种不同的怪物, 如图:
这是官方的做法, 有优有劣吧
ok, 可以出不同怪物了
ok, 现在运行一下游戏, 怪物从不同地方出来了, 可以好好的干一仗了
7. 最后,完善失败场景
- 首先添加UI, GameOver Text提示,和灰蓝色Mask, 如最后效果图
- 放好位置之后, 把Text和Mask的颜色Alpha都设置为0, 平时让玩家看不到.
- 然后给UI做动画, 等待死亡就触发它
- 选中整个HealthCanvas, 打开Window->Animation窗口, 新建一个动画
- 此时系统会自动创建一个AnimatorController, 然后会要求取名新建一个AnimationClip
- 添加Text和Mask的颜色动画, 缩放动画
- 给系统生成的HealthCanvas动画控制器做逻辑
- 做一个空动画, 然后链接到之前那个UI的AnimationClip上
- 添加一个Trigger("GameOver") 触发动画
- 这类动画记得把Exit With Time去掉勾选, 这样一触发就会立刻切换动画状态
最后添加GameOverManager组件给HealthCanvas:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class GameOverManager_ym : MonoBehaviour {
public PlayerHealth_ym playerHealth;
public float restartDelay = 3f; //重新开始的等待时间
Animator anim;
float restartTimer; ////重新开始的计时
void Start () {
anim = GetComponent<Animator> ();
}
void Update () {
if(playerHealth.currentHealth <=0) {
//触发失败动画
anim.SetTrigger ("GameOver");
//计时
restartTimer += Time.deltaTime;
if (restartTimer >= restartDelay) {
//新的场景切换, 这个demo切换原来的场景就是重新开始游戏
SceneManager.LoadScene (0);
}
}
}
}
死亡效果
ok, 玩家被怪物打死之后会有提示, 并且在3秒之后就会重新开始游戏啦
至此, 整个教程已经结束