讲解实例:3D射击游戏
注:今天所学的知识是重中之重,是Unity的基础,也是核心,掌握了本章内容,在自行设计一些玩法,在简洁的Unity框架下,理论上编写一个小游戏是很简单的,因为Unity中脚本的编写几乎都要用到今天所学的内容,万变不离其宗。让我们开始今天的学习吧。
用Unity创建游戏是由一个或多个场景(Scene)组成的,打开Unity会默认创建一个场景。
Tip:
Tip:
一个脚本可以挂载到多个物体,一个物体也可以挂载多个脚本。
什么是物体:
简单来讲,就是这些东西。
什么是组件:
简单来讲,就是这些东西。
什么是对象:
简单来讲,对象是相对而言的,一个组件所挂载的物体就是对象。
一个物体的组件也可以是对象。
组件已挂载到物体上:
using UnityEngine;
public class Test : MonoBehaviour
{
SphereCollider collider;
void Start()
{
//获取到组件后,将它的引用保存在cllider字段中,方便下次使用
collider = gameObject.GetComponent();
}
}
using UnityEngine;
public class Test : MonoBehaviour
{
SphereCollider collider;
void Start()
{
//获取到组件后,将它的引用保存在cllider字段中,方便下次使用
collider = gameObject.GetComponent();
//一下每一句的写法均等同于上一句
collider = this.gameObject.GetComponent();//this指脚本对象自身,gameObject是它的属性
collider = transform.GetComponent();//通过transform组件获取其他组件
collider = transform.GetComponent().GetComponent();
collider = transform.GetComponent().GetComponent();
//多此一举的写法,但结果不错
}
}
using UnityEngine;
public class Test : MonoBehaviour
{
SphereCollider collider;
void Start()
{
//获取到组件后,将它的引用保存在cllider字段中,方便下次使用
collider = gameObject.GetComponent();
//一下每一句的写法均等同于上一句
Test[] tests = GetComponents();
Debug.Log("共有" + tests.Length + "个Test组件");
}
}
using UnityEngine;
public class TestGameObject : MonoBehaviour
{
GameObject objMainCam;
GameObject objMainLight;
void Start()
{
objMainCam = GameObject.Find("Main Camera");
objMainLight = GameObject.Find("Directional Light");
Debug.Log("主摄像机:" + objMainCam);
Debug.Log("主光源:" + objMainLight);
//将主摄像机放在这个物体后方一米处
objMainCam.transform.position = transform.position - transform.forward;
}
}
Tip:
也可以通过禁用摄像机进行尝试。
//查找第一个标签为Player的物体
GameObject player = GameObject.FindGameObjectWithTag("Player");
//查找所有标签为Monster的物体,注意返回值是一个数组,结果可以是0个或多个
GameObject[] monsters = GameObject.FindGameObjectsWithTag("Monster");
//以上两个方法名称的区别是两者差了一个“s”,也就是说Monster是多个物体的标签
在编译器中修改标签
在脚本中修改标签:
//获得某个Player物体
GameObject p = GameObject.FindGameObjectWithTag("Player");
//将它的标签设置为Cube
p.tag = "Cube";
//判断player的标签是不是Cube
if(p.CompareTag("Cube"))
{
Debug.Log("yes");
}
//上面得CompareTag用法等价于player.tag == "Cube",推荐使用CompareTag
using UnityEngine;
public class TestGetTransform : MonoBehaviour
{
public GameObject other;
void Start()
{
if(other!=null)
{
Debug.Log("other 物体名称为" + other.name);
}
else
{
Debug.Log("未指定other物体");
}
}
}
using UnityEngine;
public class TestGetTransform : MonoBehaviour
{
public GameObject other;
public TestGetTransform otherTrans;
public MeshFilter otherMesh;
public Rigidbody otherRigid;
void Start()
{
//任意使用前面定义的变量
}
}
以上代码会改变Inspector中的信息:
(1)场景物体与预制体的关联
(2)编辑预制体
Tip:
Tip:
using UnityEngine;
public class TestInstantiate : MonoBehaviour
{
public GameObject prefab;
void Start()
{
//在场景根结点创建物体
GameObject objA = Instantiate(prefab, null);
//创建一个物体,作为当前脚本所在物体的子物体
GameObject objB = Instantiate(prefab, transform);
//创建一个物体,指定他的位置和朝向
GameObject objC = Instantiate(prefab, new Vector3(3, 0, 3), Quaternion.identity);
}
}
using UnityEngine;
public class TestInstantiate : MonoBehaviour
{
public GameObject prefab;
void Start()
{
//创建十个物体围成环
for(int i = 0; i<10; i++)
{
Vector3 pos = new Vector3(Mathf.Cos(i * (2 * Mathf.PI) / 10), 0, Mathf.Sin(i * (2 * Mathf.PI) / 10));
pos *= 5;
Instantiate(prefab, pos, Quaternion.identity);
}
}
}
using UnityEngine;
public class TestInstantiate : MonoBehaviour
{
public GameObject prefab;
void Start()
{
GameObject go = GameObject.Find("Cube");
go.AddComponent();
}
}
执行前:
执行后:因为加了物理组件受重力影响,物体已经开始下落了。
using UnityEngine;
public class TestDestrory : MonoBehaviour
{
public GameObject prefab;
void Start()
{
//创建20个物体围城环形
for(int i = 0; i < 20; i++)
{
Vector3 pos = new Vector3(Mathf.Cos(i * (2 * Mathf.PI) / 20), 0, Mathf.Sin(i * (2 * Mathf.PI) / 20));
pos *= 5;
Instantiate(prefab, pos, Quaternion.identity);
}
}
void Update()
{
if(Input.GetKeyDown(KeyCode.D))
{
GameObject cube = GameObject.Find("Cube(Clone)");
Destroy(cube);
}
}
}
void Update()
{
if(Input.GetKeyDown(KeyCode.D))
{
GameObject cube = GameObject.Find("Cube(Clone)");
Destroy(cube);
cube.AddComponent();
}
}
using UnityEngine;
public class TestInvoke : MonoBehaviour
{
public GameObject prefab;
int counter = 0;
void Start()
{
Invoke("CreatePrefab", 0.5f);
}
void CreatePrefab()
{
Vector3 pos = new Vector3(Mathf.Cos(counter * (2 * Mathf.PI) / 10), 0, Mathf.Sin(counter * (2 * Mathf.PI) / 10));
pos *= 5;
Instantiate(prefab, pos, Quaternion.identity);
counter++;
if(counter<10)
{
Invoke("CreatePrefab", 0.5f);
}
}
}
执行结果:
using UnityEngine;
public class TestDestroy : MonoBehaviour
{
public GameObject prefab;
void Update()
{
if(Input.GetKeyDown(KeyCode.D))
{
GameObject cube = GameObject.Find("Cube(Clone)");
//Destroy(cube);
//将上一句改为延迟0.8秒
Destroy(cube, 0.8f);
cube.AddComponent();
}
}
}
执行结果:
Tip:
using UnityEngine;
public class FollowCam : MonoBehaviour
{
//追踪的目标,在编辑器里指定
public Transform followTarget;
Vector3 offset;
void Start()
{
//算出从目标到摄像机的向量,作为摄像机的偏移量
offset = transform.position - followTarget.position;
}
void LateUpdate()
{
//每帧更新摄像机的位置
transform.position = followTarget.position + offset;
}
}
在这里拖拽指定追踪目标即可。
Tip:
也可以在Vector3前面加上public这样就可以在inspector里面自由调试了。
Tip:
using UnityEngine;
public class TestCoroutine : MonoBehaviour
{
void Start()
{
//开启一个协程,协程函数为Time
StartCoroutine(Timer());
}
//协程函数
IEnumerator Timer()
{
//不断循环执行,但是并不会导致死循环
while(true)
{
//打印4个汉字
Debug.Log("测试协程");
//等待一秒
yield return new WaitForSeconds(1);
//打印当前游戏经历的时间
Debug.Log(Time.time);
//在等待1秒
yield return new WaitForSeconds(1);
}
}
}
接下来做一个简易的俯视角度的射击游戏。
先搭建场景,在创建主角。
(4)创建脚本Player以控制主角移动。
using UnityEngine;
public class Player : MonoBehaviour
{
//移动速度
public float speed = 3;
//最大血量
public float maxHp = 20;
//变量,输入方向用
Vector3 input;
//是否死亡
bool dead = false;
//当前血量
float hp;
void Start()
{
//初始满血状态
hp = maxHp;
}
void Update()
{
//将键盘的横向、纵向输入,保存在input变量中
input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
//未死亡则执行移动逻辑
if(!dead)
{
Move();
}
}
void Move()
{
//先归一化输入向量,让输入更直接,同时避免斜向移动时速度超过最大速度
input = input.normalized;
transform.position += input * speed * Time.deltaTime;
//令角色前方与移动方向一致
if(input.magnitude>0.1f)
{
transform.forward = input;
}
//以上移动方式没有考虑阻挡,因此使用下面的代码限制移动范围
Vector3 temp = transform.position;
const float BORDER = 20;
if (temp.z > BORDER) { temp.z = BORDER; }
if (temp.z < -BORDER) { temp.z = -BORDER; }
if (temp.x > BORDER) { temp.x = BORDER; }
if (temp.x < -BORDER) { temp.x = -BORDER; }
transform.position = temp;
}
}
执行结果:
由测试结果可知,角色移动的范围确实是|20|
using UnityEngine;
public class FollowCam : MonoBehaviour
{
//追踪的目标,在编辑器里指定
public Transform followTarget;
Vector3 offset;
void Start()
{
//算出从目标到摄像机的向量,作为摄像机的偏移量
offset = transform.position - followTarget.position;
}
void LateUpdate()
{
//每帧更新摄像机的位置
transform.position = followTarget.position + offset;
}
}
using UnityEngine;
public class Weapon : MonoBehaviour
{
//子弹的prefab
public GameObject prefabBullet;
//几种武器的CD时间长度
public float pistolFireCD = 0.2f;//手枪
public float shotgunFireCD = 0.5f;//散弹
public float rifleCD = 0.1f;//步枪
//上次开火时间
float lastFireTime;
//当前使用哪种武器
public int curGun { get; private set; }//0 手枪,1 散弹,2 自动步枪
//开火函数,由角色脚本调用
//keyDown代表这一帧按下开火键,keyPressed代表开火键正在持续按下
//这样区分是为了实现手枪和步枪的不同手感
public void Fire(bool keyDown,bool keyPressed)
{
//根据当前武器,调用对应的开火函数
switch(curGun)
{
case 0:
if(keyDown)
{
PistolFire();
}
break;
case 1:
if(keyDown)
{
shotgunFire();
}
break;
case 2:
if(keyPressed)
{
RifleFire();
}
break;
}
}
//更换武器
public int Change()
{
curGun += 1;
if(curGun==3)
{
curGun = 0;
}
return curGun;
}
//手枪射击专用函数
public void PistolFire()
{
if (lastFireTime + pistolFireCD > Time.time)
{
return;
}
lastFireTime = Time.time;
GameObject bullet = Instantiate(prefabBullet, null);
bullet.transform.position = transform.position + transform.forward * 1.0f;
bullet.transform.forward = transform.forward;
}
//自动步枪射击专用函数
public void RifleFire()
{
if (lastFireTime + rifleCD > Time.time)
{
return;
}
lastFireTime = Time.time;
GameObject bullet = Instantiate(prefabBullet, null);
bullet.transform.position = transform.position + transform.forward * 1.0f;
bullet.transform.forward = transform.forward;
}
//散弹枪射击专用函数
public void shotgunFire()
{
if(lastFireTime+shotgunFireCD>Time.time)
{
return;
}
lastFireTime = Time.time;
//创建5颗子弹,间隔10度,扇形分布
for(int i = -2; i <= 2; i++)
{
GameObject bullet = Instantiate(prefabBullet, null);
Vector3 dir = Quaternion.Euler(0, i * 10, 0) * transform.forward;
bullet.transform.position = transform.position + dir * 1.0f;
bullet.transform.forward = dir;
//散弹枪的子弹射击距离很短,所以要修改子弹生命周期
Bullet b = bullet.GetComponent();
b.lifeTime = 0.3f;
}
}
}
(2)
(3)
(4)
using UnityEngine;
public class Bullet : MonoBehaviour
{
//子弹飞行速度
public float speed = 10.0f;
//子弹生命周期(几秒后消失)
public float lifeTime = 2;
//子弹"出生"的时间
float startTime;//上升空间小了就可以享受了
void Start()
{
startTime = Time.time;
}
void Update()
{
//子弹移动
transform.position += speed * transform.forward * Time.deltaTime;
//超过一定时间销毁自身
if(startTime+lifeTime
(5)
Weapon weapon;
void Start()
{
//初始满血状态
hp = maxHp;
weapon = GetComponent();
}
void Update()
{
//将键盘的横向、纵向输入,保存在input变量中
input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
Debug.Log(input);
bool fireKeyDown = Input.GetKeyDown(KeyCode.J);
bool fireKeyPressed = Input.GetKeyDown(KeyCode.J);
bool changeWeapon = Input.GetKeyDown(KeyCode.Q);
//未死亡则执行移动逻辑
if (!dead)
{
Move();
weapon.Fire(fireKeyDown, fireKeyPressed);
if(changeWeapon)
{
ChangeWeapon();
}
}
}
private void ChangeWeapon()
{
int w = weapon.Change();
}
我做的是这样的:
为敌人编写脚本:
using UnityEngine;
public class Enemy : MonoBehaviour
{
//用于制作死亡效果的预制体,暂时不用
public GameObject prefabBoomEffect;
public float speed = 2;
public float fireTime = 0.1f;
public float maxHp = 1;
Vector3 input;
Transform player;
float hp;
bool dead = false;
Weapon weapon;
void Start()
{
//根据Tag查找玩家物体
player = GameObject.FindGameObjectWithTag("Player").transform;
weapon = GetComponent();
}
void Update()
{
Move();
Fire();
}
void Move()
{
//玩家的input是从键盘输入而来,而敌人的input是始终指向玩家方向
input = player.position - transform.position;
input = input.normalized;
transform.position += input * speed * Time.deltaTime;
if(input.magnitude>0.1f)
{
transform.forward = input;
}
}
void Fire()
{
//一直开枪,开枪的频率可以通过武器启动控制
weapon.Fire(true, true);
}
private void OnTriggerEnter(Collider other)
{
//稍后实现
}
}
//当子弹碰到其他物体时触发
private void OntriggerEnter(Collider other)
{
//如果子弹的Tag与被碰撞的物体的Tag相同,则不算打中
//这是为了防止同类子弹相互碰撞抵消
if(CompareTag(other.tag))
{
return;
}
Destroy(gameObject);
private void OnTriggerEnter(Collider other)
{
if(other.CompareTag("EnemyBullet"))
{
if (hp <= 0) { return; }
hp--;
if (hp <= 0) { dead = true; }
}
}
private void OnTriggerEnter(Collider other)
{
if(other.CompareTag("PlayerBullet"))
{
Destroy(other.gameObject);
hp--;
if(hp<=0)
{
dead = true;
//这里可以添加死亡特效
// Instantiate(prefabBoomEffect,transform.position,transform.rotation);
Destroy(gameObject);
}
}
}
using UnityEngine;
using System.Collections.Generic;
public class BoomEffect : MonoBehaviour
{
List objs = new List();
const int N = 15;
void Start()
{
for(int i=0;i