本文是学习官方教程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烘焙

  1. 首先把场景中需要烘焙的物件设置为static, 一些障碍物体要添加碰撞体
  2. 点击Window->Navigation面板,设置烘焙属性并且烘焙

创建一个怪物并且基础设置

  1. 设置Rigidbody
  2. 添加一个胶囊碰撞体,CapsuleCollider组件,用于寻找玩家做碰撞
  3. 添加一个球碰撞体,SphereCollider组件,用于以后攻击玩家做碰撞
  4. 添加一个寻路组件,NavMeshAgent,用于配合Navigation寻路
  5. 给怪物设置一个动画控制机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如下图效果

  1. 在场景中创建一个Canvas
  2. 里面创建一个空物体用来存放HealthUI
  3. 创建一个Slider,删除手柄,只要进度条,用来显示血量


如图,左下角有一个红心,和一个Slider,Slider删掉了手柄

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. 给玩家添加攻击动作,和怪物生命脚本

首先配置资源

  1. 把玩家射击的粒子预设组件拷贝到玩家的枪上
  2. 给玩家的枪添加一个Line Renderer组件
  • 设置Parameters的StartWidth和EndWidth为0.07
  • 设置Materials->Element0的材质为LineRenderMaterial
  1. 给玩家的枪添加一个Light组件,并且设置好亮度角度, 初始取消它,等待攻击的时候再临时激活
  2. 把怪物受到伤害的粒子组件拷贝到怪物身上
  3. 把怪物的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

  1. 在场景中间添加一个Text,如图:
  2. 给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:

  1. 把ZomBear的模型拖进Scene中
  2. 把Zombunny的所有组件拷贝到ZomBear上: 拷贝所有组件的方法扩展链接
  3. 因为两个动画状态机和Avatar(化生)都是一样的,所以动画状态机可以使用同一个

第二个怪物Hellephant:

  1. 把Hellephant的模型拖进Scene中
  2. 把Zombunny的所有组件拷贝到Hellephant上
  3. 因为两个动画的Avatar不一样, 所以播放的动画不能重用, 但是动画逻辑可以重用
  • 创建一个Animator Override Controller(动画覆盖控制器)
  • 动画控制器选ZombunnyAC, Original中的动画片段选用Hellephant自己的动画
  • 这样就覆盖动画片段, 并且使用原有的动画逻辑了

修改一下两个新怪物的属性,需要修改的内容有:

  • 移动速度
  • 生命值
  • 攻击力, 攻击间隔
  • 收到伤害声音, 死亡声音

接下来创建怪物生成器的代码

创建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);
    }
}

  1. 在场景中创建一个空物体, 把坐标还原成(0, 0, 0), 取名为EnemyManager
  2. 在EnemyManager中创建三个空物体, 并且拖动到场景不同的位置中, 用于设定3种怪物的生成位置
  3. 在EnemyManager中绑定三个EnemyManager组件, 用于创建三种不同的怪物, 如图:

ok, 现在运行一下游戏, 怪物从不同地方出来了, 可以好好的干一仗了

7. 最后,完善失败场景

  1. 首先添加UI, GameOver Text提示,和灰蓝色Mask, 如最后效果图
  2. 放好位置之后, 把Text和Mask的颜色Alpha都设置为0, 平时让玩家看不到.
  3. 然后给UI做动画, 等待死亡就触发它
  • 选中整个HealthCanvas, 打开Window->Animation窗口, 新建一个动画
  • 此时系统会自动创建一个AnimatorController, 然后会要求取名新建一个AnimationClip
  • 添加Text和Mask的颜色动画, 缩放动画
  1. 给系统生成的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秒之后就会重新开始游戏啦

至此, 整个教程已经结束