更新于 

2D Shooting

子弹 && 射线

实现Shooting的两种方式
  • 预制件 prefabs
    • 创建Bullet Prefabs,每次射击都创建一个新的子弹实例
    • 子弹本体控制射出等动作、伤害、效果等属性
  • 射线 raycasts
    • 由射击点射出一条射线,检测是否存在碰撞物

开火点 FirePoint

FirePoint

在Player层级下创建空对象FirePoint,
用于对开火点坐标进行标注。

需要注意的是,在控制角色移动的脚本CharacterController2D中,
实现角色水平移动时翻转的函数是这样的:

1
2
3
4
5
6
7
8
9
private void Flip()
{
m_FacingRight = !m_FacingRight;

// Multiply the player's x local scale by -1.
Vector3 theScale = transform.localScale;
theScale.x *= -1; // 反转x轴缩放
transform.localScale = theScale;
}

通过反转Player在x轴方向的缩放值进行翻转。
这种方式能够在不影响Player坐标系的情况下对它的贴图进行翻转。
但是Player下的FirePoint的坐标系是动态的,
在Player进行翻转时,FirePoint坐标系也需要跟随翻转,
这样才能保证子弹一直朝Player面向的方向进行射击,
通过将Player沿y轴方向旋转180°的方式能够实现这一点:

1
2
3
4
private void Flip(){
m_FacingRight = !m_FacingRight;
transform.Rotate(0f, 180f, 0f);
}

Prefab射击

Bullet

使用prefab方式,就要做bullet的prefab:

BulletPrefab脚本:

  • Bullet 为子弹自定义行为的脚本
  • Circle Collider 2D 碰撞体积
    • isTrigger = true 仅用于触发
  • Rigidbody 2D
    • Graivity Scale = 0 无重力
    • Collision Detection = Continuous 持续检测
    • Freeze Rotation Z = true 无z轴旋转

将做好的BulletPrefab拖动到Assets目录下,即作为预制件使用。

创建子弹

射击创建BulletPrefab

子弹已经做好,那么只要在Player射击的时候创建出子弹即可,
因此需要为Player创建新的Weapon脚本,
Weapon脚本要实现在用户射击时创建BulletPrefab的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public Transform firePoint; // 火点 此处赋值Player下的FirePoint
public GameObject bulletPrefab; // 子弹预制件 此处赋值Bullet预制件
void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}

// 射击
private void Shoot() {
// 创建预制件
Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
}
}

创建实例的方法:

1
2
// 创建对象 位置 旋转
Instantiate(GameObject. position, rotation)

子弹移动

子弹移动

通过为Bullet添加初始速度来时其保持移动,
这要通过子弹的Rigidbody2D.velocity属性来控制:

1
2
3
4
5
6
7
8
9
10
public class Bullet : MonoBehaviour
{
public float speed = 30f; // 速度
public Rigidbody2D rd; // 使用Rigidbody2D.velocity属性,为物体提供初始速度

void Start()
{ // 速度 = 移动方向 * 移动速度
rd.velocity = transform.right * speed;
}
}

子弹伤害

子弹伤害

现在子弹已经能射出,接下来要实现的:

  • 检测子弹是否碰撞到物体
    • 检测到碰撞,产生子弹冲击效果,子弹消失
    • 碰撞物是否是敌人
      • 使敌人,造成伤害
  • 敌人脚本要实现的动作
    • 收到伤害
      • 生命值为0时,死亡
    • 死亡
      • 敌人消失
      • 出现消失特效
Enemy脚本

为敌人添加的Enemy脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Enemy : MonoBehaviour
{
public int health = 100; // 生命值
public GameObject DeathEffect; // 死亡效果

public void TakeDamage(int damage) {
health -= damage;
if (health <= 0)
{
Death();
}
}

private void Death() {
// Quaternion.identity代表无旋转
Instantiate(DeathEffect, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}

其中的TakeDamage方法的调用时机是:当子弹碰撞到Enemy时,
这一步在Bullet预制件脚本中进行,
因此需要用public暴露出来。

子弹碰撞检测

之前已经打开了Bullet预制件Collider2D的isTrigger选项,
因此当子弹碰撞到物体时,会触发Unity提供的OnTriggerEnter2D函数,
这个函数提供一个Collider2D类的参数,表示碰撞物信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Bullet : MonoBehaviour
{
public int damage = 40; // 伤害
public GameObject impactEffect; // 子弹冲击效果预制件
// Trigger回调函数
private void OnTriggerEnter2D(Collider2D hitInfo)
{
// 判断碰撞物是否是Enemy
Enemy enemy = hitInfo.GetComponent<Enemy>();
if(enemy != null)
{
enemy.TakeDamage(damage);
}
Instantiate(impactEffect, transform.position, transform.rotation); // 冲击效果
Destroy(gameObject); // 销毁子弹
}
}

Raycast射击

raycast射线检测

raycast

raycast和prefab不同的地方是:

  • prefab碰撞的逻辑需要再子弹上实现
  • raycast碰撞的逻辑在Player上就能实现

为Player添加WeaponRaycast脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class WeaponRaycast : MonoBehaviour
{
public Transform firePoint;
public int damage = 30;
public GameObject ImpactEffect;

// Update is called once per frame
void Update()
{
if (Input.GetButtonDown("Fire1")) {
Shoot();
}
}
private void Shoot()
{
RaycastHit2D hitInfo = Physics2D.Raycast(firePoint.position, firePoint.right);
if (hitInfo)
{
Enemy enemy = hitInfo.transform.GetComponent<Enemy>();
if(enemy != null)
{
enemy.TakeDamage(damage);
}
Instantiate(ImpactEffect,hitInfo.point,Quaternion.identity);
}
}
}

创建raycast并返回射线探测物:

1
RaycastHit2D hitInfo = Physics2D.Raycast(firePoint.position, firePoint.right);

原本在Prefab方法里,会在OnTriggerEnter2D方法中提供一个Collider2D类型的参数,
这里对应RaycastHit2D中的参数为:

1
Enemy enemy = hitInfo.transform.GetComponent<Enemy>();

而碰撞点的坐标值则从hitInfo.point里取得

射线绘制

Line

使用Line(Effect/Line)来作为射线效果,
在Player下创建Line实例,

开启Line的Use World Space属性

Coroutine射线效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class WeaponRaycast : MonoBehaviour
{
// ...

private void Start()
{
lineRenderer.enabled = false; // 开始时关闭射线可见
}
void Update()
{
if (Input.GetButtonDown("Fire1")) {
StartCoroutine(Shoot()); // StartCoroutine调用Shoot
}
}
IEnumerator Shoot() // IEnumerator声明
{
// ...碰撞检测略

// 实现射线短暂显示的效果
lineRenderer.enabled = true;

// 实现短暂延时
yield return new WaitForSeconds(0.01f);

lineRenderer.enabled = false;
}
}

Top Down Shooting 俯视角射击

这里俯视角下玩家的移动涉及到跟随鼠标移动的旋转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class PlayerMovement : MonoBehaviour
{
public float moveSpeed = 5f; // 移动速度
public Rigidbody2D rb; // rigidbody

Vector2 movement; // 位置
public Animator animator; // 动画控制器

public Camera cam; // 相机
Vector2 mousePos; // 鼠标位置

// Update用于获取玩家输入
void Update() {
movement.x = Input.GetAxisRaw("Horizontal"); // 获取横坐标
movement.y = Input.GetAxisRaw("Vertical"); // 获取纵坐标

// 动画
float moveDiff = Mathf.Abs(movement.x * moveSpeed) + Mathf.Abs(movement.y * moveSpeed); // 相对移动距离
animator.SetFloat("speed", moveDiff);

// 将鼠标相对屏幕的坐标转换为世界点位坐标
mousePos = cam.ScreenToWorldPoint(Input.mousePosition);
}

// FixedUpdate用于对玩家位置更新
private void FixedUpdate()
{
rb.MovePosition(rb.position + movement * moveSpeed * Time.fixedDeltaTime);

// 计算z轴旋转角
Vector2 lookDir = mousePos - rb.position;
float angle = Mathf.Atan2(lookDir.y, lookDir.x) * Mathf.Rad2Deg + 90f;
rb.rotation = angle;
}
}
  • Input.GetAxisRaw(“Horizontal|Vertical”) 获取横向/纵向移动距离
  • Camera.ScreenToWorldPoiint(Vector2D var) 将鼠标相对屏幕的坐标转换为世界坐标系下的坐标

最后需要根据玩家当前坐标和鼠标位置计算旋转的角度:

  • Mathf.Atan2(y,x) 获取正弦值为y/x的弧度
  • Mathf.Rad2Deg 将弧度转为角度需要乘上的数
Shooting脚本

Shooting脚本需要在玩家触发子弹发射行为时,

  • 获取开火点
  • 创建子弹实例
  • 给子弹一个推动力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Shooting : MonoBehaviour
{
public GameObject Bullet; // 子弹prefab
public Transform FirePoint; // 开火点
public float bulletForce = 20f; // 子弹速度

private void Update()
{
if (Input.GetButtonDown("Fire1")) {
Shoot();
}
}

private void Shoot() {
GameObject bullet = Instantiate(Bullet, FirePoint.position, FirePoint.rotation);
Rigidbody2D rb = bullet.GetComponent<Rigidbody2D>();
rb.AddForce(-FirePoint.up * bulletForce, ForceMode2D.Impulse);
}
}
Bullet

最后是子弹自身检测到碰撞后的逻辑:

1
2
3
4
5
6
7
8
9
10
11
public class Bullet : MonoBehaviour
{
public GameObject ImpactEffect;

private void OnCollisionEnter2D(Collision2D collision)
{
GameObject impactEffect = Instantiate(ImpactEffect, transform.position, Quaternion.identity);
Destroy(impactEffect, 0.5f); // 半秒后销毁特效实例
Destroy(gameObject); // 销毁当前实例
}
}