Unity第一人称射箭游戏制作

一、游戏介绍

使用Unity制作一个第一人称射箭游戏。

  • 玩家使用WASD移动
  • 使用空格键拉弓,只有在指定射击区域内才能拉弓
  • 使用鼠标左键射击
  • 使用鼠标右键打开瞄准镜
  • 使用F1切换鸟瞰图视角,便于定位当前位置

游戏中有两个射击场,一个在迷宫尽头,一个在山顶。射击场分别有两个运动靶和两个静态靶,射中运动靶加十分,射中静态靶加二十分。


二、游戏制作

1. 资源导入

导入四个资源包,分别是十字弩、天空盒、植物、靶子。

资源

资源


2. 设置天空盒

随着时间流逝,游戏内天空盒应当自行切换。编写天空盒控制脚本:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkyBoxController : MonoBehaviour
{
public Material[] skyboxes; // 天空盒材质数组
private int currentSkyboxIndex = 0; // 当前天空盒索引
public float switchInterval = 5f; // 切换间隔时间

private void Start()
{
if (skyboxes.Length == 0)
{
Debug.LogError("请在Inspector中分配天空盒材质!");
return;
}
RenderSettings.skybox = skyboxes[currentSkyboxIndex]; // 设置初始天空盒
InvokeRepeating("SwitchSkybox", switchInterval, switchInterval);
}

private void SwitchSkybox()
{
currentSkyboxIndex = (currentSkyboxIndex + 1) % skyboxes.Length;
RenderSettings.skybox = skyboxes[currentSkyboxIndex]; // 应用新天空盒
Debug.Log($"切换到天空盒: {skyboxes[currentSkyboxIndex].name}");
}
}

在Hierarchy面板中创建空对象,命名为skyboxController,绑定天空盒脚本。

天空盒


3. 绘制地形

在Hierarchy面板中创建Terrain对象。使用Inspector面板中的Terrain组件,导入先前素材包中的地面材质、植物等素材。用Plaint Texture设置地形材质,使用Set Height绘制山,使用Smooth Height绘制斜坡,使用Plant Trees和Plant Detail绘制其他细节。

迷宫

山路

山坡


4. 背景音乐

创建一个空对象,命名为Music Manager,添加Audio Source组件和脚本组件,绑定背景音乐脚本,并把指定的音乐拖入选择框。

音乐管理器

游戏开始后,会自动循环播放背景音乐。


5. 玩家

创建一个3D对象命名Player。为了实现第一人称效果,需把主摄像机拖动到Player对象底下,再把十字弩拖动到主摄像机底下。

玩家组件结构

将弩弓和摄像机调整至合适位置

给Player对象增加碰撞组件和刚体组件,并把Freeze Rotation的x、y、z轴都勾选,避免不必要的旋转。绑定Camera Controller脚本,用于接收键盘输入,控制玩家游走。

玩家面板


6. 多摄像机

为了增加射击体验,玩家按右键时能瞄准,并在视野中显示准星;玩家按下F1时,画面右上角会出现鸟瞰图,标识玩家目前位置。

新建一个TopDownCamera相机作为鸟瞰图相机,将X轴旋转调整为90度,使其朝向地面。然后为其添加CameraFollow脚本,让它保持在目标物体(玩家)上方50个单位处。

新建一个RenderTexture,命名为TopDownRenderTexture,将其绑定到TopDownCamera的Target Texture中。这样,鸟瞰图摄像机的画面将被渲染到Texture里。再在Hierarchy面板里新建一个Raw Image,在Texture里绑定TopDownRenderTexture。在Rect Transform组件内调整Image的位置,让它固定在画面右上角。

鸟瞰图摄像机

新建一个Camera Manager空对象,绑定Camera Switch脚本以及其他相关对象,实现按下F1时展示鸟瞰图。

摄像机管理组件

最终实现如图:

右上角显示鸟瞰图


7. 靶场

给静态靶和动态靶都加上标签,以便区分。网格碰撞体为资源自带。给动态靶额外加上刚体组件,并勾选is Kinematic。为动态靶绑定移动动画。将其放置到场景合适位置。

静态靶

动态靶


8. 射击

使用动画实现拉弓射箭效果。其中hold是混合树。

动画

混合树

在弩弓上绑定Archer Controller,射箭预备代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void Update()
{
// 右键按下,开启瞄准
if (Input.GetMouseButtonDown(1))
{
isAiming = true;
}
// 右键松开,取消瞄准
if (Input.GetMouseButtonUp(1))
{
isAiming = false;
}

// 平滑过渡视角
if (maincam != null)
{
float targetFOV = isAiming ? aimFOV : normalFOV;
maincam.fieldOfView = Mathf.Lerp(maincam.fieldOfView, targetFOV, aimTransitionSpeed * Time.deltaTime);
}

// 检查是否在允许射击的范围内
if (IsInArea())
{
// 按住空格键:拉弓
if (Input.GetKeyDown(KeyCode.Space))
{
pullDuration = 0;
isPulling = true;
animator.SetBool("isPulling", true);
animator.SetBool("isShooting", false);
animator.SetBool("isHolding", false);
}

// 持续按住空格键计算拉力
if (isPulling)
{
pullDuration += Time.deltaTime;
pullStrength = Mathf.Clamp01(pullDuration / maxPullDuration);
animator.SetFloat("pullStrength", pullStrength); // 更新Animator中的参数
}

// 松开空格键:保持当前拉力状态
if (Input.GetKeyUp(KeyCode.Space))
{
isPulling = false;
animator.SetBool("isPulling", false);
animator.SetBool("isHolding", true);
}

// 按下鼠标左键:发射箭矢
if (Input.GetMouseButtonDown(0) && animator.GetBool("isHolding"))
{
animator.SetBool("isShooting", true);
animator.SetBool("isHolding", false);

// 动画事件触发箭矢发射
}
}
}

射击代码:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public void FireArrow()
{
// 检查射箭区域
Vector3 currentPosition = transform.position;
Vector3 area1 = new Vector3(489f, 46f, 593f);
Vector3 area2 = new Vector3(666f, 4f, 343f);
float radius = 10f;

bool inArea1 = Vector3.Distance(currentPosition, area1) <= radius;
bool inArea2 = Vector3.Distance(currentPosition, area2) <= radius;

if (inArea1 && area1Shots > 0)
{
area1Shots--; // 减少区域1箭数
Debug.Log($"区域1剩余箭数:{area1Shots}");
}
else if (inArea2 && area2Shots > 0)
{
area2Shots--; // 减少区域2箭数
Debug.Log($"区域2剩余箭数:{area2Shots}");
}
else
{
Debug.LogWarning("当前区域无箭数可用!");
return;
}

// 创建箭矢对象
GameObject arrow = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Arrow"));
arrow.AddComponent<ArrowController>();
ArrowController arrowController = arrow.GetComponent<ArrowController>();
arrowController.hitSound = Resources.Load<AudioClip>("Sound/hit");
arrowController.cam = maincam;

arrow.transform.position = firePoint.transform.position;
arrow.transform.rotation = Quaternion.LookRotation(this.transform.forward);

Rigidbody rd = arrow.GetComponent<Rigidbody>();
if (rd != null)
{
rd.AddForce(this.transform.forward * 1 * pullStrength, ForceMode.Impulse);

// 播放射箭音效
if (arrowShootSound != null)
{
audioSource.PlayOneShot(arrowShootSound);
Debug.Log($"播放了射箭音效");
}
}


// 动画回到Empty状态
animator.SetBool("isShooting", false);
}

击中运动靶后,运动靶会有自然下坠效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void TriggerTargetReaction(GameObject target)
{
// 停止动画播放
Animator targetAnimator = target.GetComponent<Animator>();
if (targetAnimator != null)
{
targetAnimator.enabled = false; // 停止动画
}

// 设置靶子为可下落状态
Rigidbody targetRb = target.GetComponent<Rigidbody>();
if (targetRb != null)
{
targetRb.isKinematic = false; // 解除刚体固定
targetRb.useGravity = true; // 启用重力
}

Destroy(target, 2f); // 2 秒后销毁靶子
}

三、演示视频

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2024 Boreascup

请我喝杯咖啡吧~

支付宝
微信