一、游戏设计要求
- 游戏场景(14分)
- 地形(2分):使用地形组件,上面有山、路、草、树;(可使用第三方资源改造)
- 天空盒(2分):使用天空盒,天空可随 玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶(2分):使用静态物体,有一个以上固定的靶标;(注:射中后状态不会变化)
- 运动靶(2分):使用动画运动,有一个以上运动靶标,运动轨迹,速度使用动画控制;(注:射中后需要有效果或自然落下)
- 射击位(2分):地图上应标记若干射击位,仅在射击位附近或区域可以拉弓射击,每个位置有 n 次机会;
- 摄像机(2分):使用多摄像机,制作 鸟瞰图 或 瞄准镜图 使得游戏更加易于操控;
- 声音(2分):使用声音组件,播放背景音 与 箭射出的声效;
- 运动与物理与动画(8分)
- 游走(2分):使用第一人称组件,玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;(注:建议使用 unity 官方案例)
- 射击效果(2分):使用 物理引擎 或 动画 或 粒子,运动靶被射中后产生适当效果。
- 碰撞与计分(2分):使用 计分类 管理规则,在射击位射中靶标得相应分数,规则自定;(注:应具有现场修改游戏规则能力)
- 驽弓动画(2分):使用 动画机 与 动画融合, 实现十字驽蓄力半拉弓,然后 hold,择机 shoot;
- 游戏与创新(不限项,每项 2 分)
- 场景与道具类: 有趣的事物 或 模型等 (可以模仿 Unity 官方案例,在地形基础上搭建几何型场地)
- 效果类:如显示箭的轨迹,特殊声效,等
- 力场类: 如运用力场实现 ai 导航 与 捕获等
- 玩法类:
- 游戏感: 这是一个模糊的指标,有游戏感也许是某些人的天赋
二、游戏源代码和视频
水个游戏剧情:史蒂夫从地狱门回到主世界,发现白天也出现了不会燃烧的僵尸和不会动的小白,他手上只有一把弩和32支箭,好在弓弩附过魔,一箭可以击败敌人。
游戏胜利目标:尽可能多击杀怪物,并到达村庄钻石傀儡处。
视频:unity大作业-第一人称射击_哔哩哔哩_bilibili
三、游戏设计过程
1. 地形
(1)使用Unity的Terrain组件,用笔刷的Raise or Lower Terrain功能刷出一定高度,作为背景;
(2)使用MineWays程序,导出我的世界存档地图模型,导入unity,并进行渲染,具体教程参考:Unity导入我的世界模型,简单图解教程(包括调整材质贴图和光照和添加碰撞)_mineways-优快云博客
2. 天空盒
游戏内一共设置了3个天空盒:白天(daySkybox),黄昏(duskSkybox),夜晚(nightSkybox)。通过输入3条指令:/time set day, /time set dusk, /time set night分别切换。
具体代码如下:
SkyboxSwitcher 类
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class SkyboxSwitcher : MonoBehaviour
{
public Material daySkybox; // 白天天空盒材质
public Material duskSkybox; // 黄昏天空盒材质
public Material nightSkybox; // 夜晚天空盒材质
public TMP_InputField commandInput; // 输入框,用于输入命令
public bool entering = false; // 标志位,输入时禁用wasd操控玩家运动
public bool enterPressed = false; // 标志位,判断按下 Enter 键时是显示输入框还是执行命令,隐藏输入框
private void Start()
{
// 初始化时禁用命令输入框
commandInput.gameObject.SetActive(false);
// 启动时不需要激活输入框
commandInput.onEndEdit.AddListener(OnCommandEntered);
}
private void Update()
{
// 如果按下 Enter 键
if (Input.GetKeyDown(KeyCode.Return))
{
Debug.Log("Press Enter");
if (!enterPressed)
{
// 第一次按 Enter 时,显示输入框并激活
commandInput.gameObject.SetActive(true);
commandInput.ActivateInputField(); // 激活输入框,准备输入命令
enterPressed = true; // 标记为第一次按下 Enter
entering = true; //开启输入模式
}
else
{
// 第二次按 Enter 时,执行命令并切换天空盒
ExecuteCommand(commandInput.text);
ClearInputField(); // 清空输入框并隐藏
enterPressed = false; // 重置标志位,等待下一次输入
entering = false; //退出输入模式,玩家可以活动
}
}
}
// 处理输入的命令
private void ExecuteCommand(string input)
{
// 去除前后空格
input = input.Trim();
// 根据命令切换天空盒
if (input == "/time set day")
{
SwitchSkybox(daySkybox); // 切换到白天天空盒
}
else if (input == "/time set dusk")
{
SwitchSkybox(duskSkybox); // 切换到黄昏天空盒
}
else if (input == "/time set night")
{
SwitchSkybox(nightSkybox); // 切换到夜晚天空盒
}
else
{
Debug.Log("未知命令: " + input); // 如果命令未知,输出调试信息
}
}
// 切换天空盒的通用方法
private void SwitchSkybox(Material skybox)
{
RenderSettings.skybox = skybox; // 切换天空盒
DynamicGI.UpdateEnvironment(); // 更新环境光
}
// 清空输入框并隐藏
private void ClearInputField()
{
commandInput.text = ""; // 清空输入框
commandInput.gameObject.SetActive(false); // 隐藏输入框
}
// 监听命令输入结束
private void OnCommandEntered(string input)
{
// 由于按 `Enter` 键会调用这个方法,在 `Update` 中已经处理了执行命令逻辑,这里不再需要额外处理
}
}
3. 玩家的移动和视角旋转
通过将玩家模型、摄像头和弓弩模型绑定,实现弓弩游走的效果,通过玩家动作类脚本进行水平、垂直方向上的移动,重力的设置以及摄像头视角随鼠标移动的旋转,同时还负责实现玩家被怪物攻击时的击退效果。
PlayerMovementController 类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovementController : MonoBehaviour
{
// 定义角色控制器(用于处理角色的物理碰撞和移动)
CharacterController playerController;
//玩家生命值管理器
public PlayerDamageController playerDamageController;
// 用于存储角色的移动方向
Vector3 direction;
//输入栏输入模式,禁用wasd移动
public SkyboxSwitcher skyboxSwitcher;
// 角色的基本移动速度
public float speed = 6;
// 跳跃的力量(影响跳跃的高度)
public float jumpPower = 0.0001f;
// 重力的大小,用于控制跳跃后的下落速度
public float gravity = 10f;
// 击退的速度(瞬时施加的力)
public Vector3 knockbackVelocity;
// 受伤后,击退持续时间
public float knockbackDuration = 0.5f;
public float knockbackTime = 0f;
// 鼠标的移动速度,用于控制视角旋转的速度
public float mousespeed = 5f;
// 鼠标旋转的上下限制角度
public float minmouseY = -90f;
public float maxmouseY = 90f;
// 记录当前视角的X和Y旋转角度
float RotationY = 0f;
float RotationX = 0f;
// 第一人称摄像机
public Transform agretctCamera;
// 俯瞰摄像机
public Transform topDownCamera;
//获得音频
private AudioSource audioSource;
public AudioClip hurtSound;
// 在游戏开始时获取CharacterController组件
void Start()
{
// 获取胶囊体的 CharacterController 组件
playerController = this.GetComponent<CharacterController>();
}
// 每一帧更新时调用,用于处理玩家的移动和视角控制
void Update()
{
if ((skyboxSwitcher.entering))
{
return;
}
// 获取玩家输入的水平和垂直轴(WASD或箭头键)
float _horizontal = Input.GetAxis("Horizontal");
float _vertical = Input.GetAxis("Vertical");
// 初始化方向为零向量
direction = Vector3.zero;
// 只有在玩家按下键时才更新移动方向
if (_horizontal != 0 || _vertical != 0)
{
direction = new Vector3(_horizontal, 0, _vertical);
}
// 根据玩家是否按下Shift或Ctrl调整速度
if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
{
speed = 3; // 按下Shift时减慢速度
}
else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))
{
speed = 12; // 按下Ctrl时加速
}
else
{
speed = 6; // 默认速度
}
// 如果角色在地面上
if (playerController.isGrounded)
{
// 如果按下空格键,则角色跳跃
if (Input.GetKeyDown(KeyCode.Space))
direction.y = jumpPower; // 设置跳跃力度
}
// 每帧更新重力,使角色逐渐下落
direction.y -= gravity * Time.deltaTime;
// 将计算出来的移动方向转换到世界坐标系下并执行移动
playerController.Move(playerController.transform.TransformDirection(direction * Time.deltaTime * speed));
// 在这里施加击退效果
if (knockbackTime > 0)
{
// 施加击退力
playerController.Move(knockbackVelocity * Time.deltaTime);
knockbackTime -= Time.deltaTime; // 击退时间减少
}
// 获取并更新鼠标输入,控制玩家左右旋转(旋转角度累加)
RotationX += Input.GetAxis("Mouse X") * mousespeed;
// 获取鼠标上下移动输入,控制相机的上下旋转(限制在设定的范围内)
RotationY -= Input.GetAxis("Mouse Y") * mousespeed;
RotationY = Mathf.Clamp(RotationY, minmouseY, maxmouseY); // 限制相机的上下旋转角度在设定范围内
// 更新玩家的水平旋转(绕Y轴旋转)
this.transform.eulerAngles = new Vector3(0, RotationX, 0);
// 更新相机的旋转,控制相机的上下视角旋转
agretctCamera.transform.eulerAngles = new Vector3(RotationY, RotationX, 0);
// 俯瞰视角:摄像机位于角色上方,并始终注视玩家
//topDownCamera.LookAt(this.transform); // 始终注视玩家
}
public void PlayerDamaged(Vector3 knockbackDirection, float knockbackStrength)
{
Debug.Log("Attacked++!");
// 计算击退效果,方向是击退方向,力度由传入的参数决定
knockbackVelocity = knockbackDirection.normalized * knockbackStrength;
// 设置击退时间
knockbackTime = knockbackDuration;
//播放音频
AudioSource.PlayClipAtPoint(hurtSound, Vector3.zero);
//扣一点血
playerDamageController.TakeDamage(1);
}
}
4. 射击位
在游戏中,玩家开始不能自由游走射击,必须在规定的射击位击杀三个骷髅,才能获得自由射击的能力。
具体实现:
比较 玩家与射击位中心的距离 和 射击位的半径,判断是否进入射击位;
射杀3个骷髅,将布尔变量FinishedShootArea修改为true,允许自由射击。
代码如下:
IfShootArea类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class IfShootArea : MonoBehaviour
{
public Transform player; // 玩家对象
public bool InShootArea = false; // 是否在射击位
public Vector3 targetPosition = new Vector3(63f, 30f, -45f); // 射击位圆心
public float shootingAreaRadius = 2f; // 射击位范围半径
public bool FinishedShootArea = false; // 是否在射击位击杀三个骷髅
public int SkeletonDeadCount = 3; //骷髅数量
void Start()
{
}
void Update()
{
}
//判断是否在射击位内
public void CheckIfInArea()
{
float distanceToTarget = Vector3.Distance(player.position, targetPosition);
//Debug.Log("距离是?" + distanceToTarget);
if (distanceToTarget <= shootingAreaRadius)
{
InShootArea = true;
}
else
InShootArea = false;
}
//检查是否射杀三个骷髅
void CheckIfShootSkeleton()
{
if (SkeletonDeadCount == 0)
FinishedShootArea = true;
}
//骷髅被击杀
public void changeDeadCount(int count)
{
SkeletonDeadCount -= count;
}
//判断能否射击
public bool ifShoot()
{
CheckIfInArea();
CheckIfShootSkeleton();
//Debug.Log("可以射击吗?" + (InShootArea || FinishedShootArea));
return (InShootArea || FinishedShootArea);
}
}
5. 弓弩射击
(1)动画设计
动画状态:empty(空膛),half(半拉弓和拉满),shoot(射击),
通过改变布尔变量isPulling和isShooting,实现empty到half、half到shoot到empty的切换;
混合树half:empty为空膛,fill为拉满,混合两个动作实现半拉弓,
通过浮点型变量PullStrength实现拉弓逐渐拉满的平滑过渡。
(2)脚本设计
射击前提条件:箭支数量大于0,位于射击位或击杀三个骷髅;
拉弓动作:右键按下开始计时,右键按压过程不断更新拉弓时间,右键放开完成记录;
设置射速和射程的最值,用拉弓时间和最大拉弓时间的比值进行对射速、射程、动画状态变量PullStrength进行线性映射;
射击动作:点击左键发射,创建箭支实例,箭支以摄像头方向即屏幕中间十字准星为方向进行匀速运动。
ArcherController 类
using UnityEngine;
using System.Collections; // 引入协程所需的命名空间
public class ArcherController : MonoBehaviour
{
private Animator animator; //动画组件
public Transform player; // 玩家对象
public Transform firePoint; //箭支射出点
private bool Loaded = false; // 是否正在装填
private AudioSource audioSource; //音频
public AudioClip fillSound;
public AudioClip shootSound;
float arrowDistance; //射程
float minArrowDistance = 10f; //最短射程
float maxArrowDistance = 100f; //最长射程
float pullDuration = 0f; //拉弓时间
public float maxPullDuration = 2f; // 最长拉弓时间
public float arrowSpeed; // 箭速
public float minArrowSpeed = 5f; // 最小箭速
public float maxArrowSpeed = 30f; // 最大箭速
private float rightDownTime; //右键按下时间
private float rightUpTime; //右键松开时间
private bool isRightMouseButtonPressed = false; //右键是否正在按着
public ArrowCountController arrowCountController; //引用对象,修改箭的数量
public IfShootArea ifShootArea; //引用对象,获得射击条件
//射杀三个骷髅作为可以在射击位外自由射击的条件
void Start()
{
// 获取 Animator 组件
animator = GetComponent<Animator>();
//获取挂载音频
audioSource = GetComponent<AudioSource>(); // 获取挂载的 AudioSource
}
void Update()
{
if (ifShootArea.ifShoot())
shoot();
}
//射击动作
void shoot()
{
if (arrowCountController.arrowcount > 0)
{
// 右键点击,进行装填
if (Input.GetMouseButtonDown(1) && !Loaded)
{
//重置力度
pullDuration = 0f;
//改变动画状态变量
animator.speed = 1;
animator.SetBool("isLoading", true);
animator.SetBool("isShooting", false);
animator.SetFloat("PullStrength", 0);
//播放音频
AudioSource.PlayClipAtPoint(fillSound, Vector3.zero);
//记录右键按下
isRightMouseButtonPressed = true;
//箭数-1
arrowCountController.changeArrow(-1);
}
//右键按压,计算拉弓力度
if (isRightMouseButtonPressed)
{
// 更新拉弓时间
if (pullDuration < maxPullDuration)
pullDuration += Time.deltaTime;
//线性映射拉弓时间至射速
arrowSpeed = Mathf.Lerp(minArrowSpeed, maxArrowSpeed, pullDuration / maxPullDuration);
//线性映射拉弓时间至射程
arrowDistance = Mathf.Lerp(minArrowDistance, maxArrowDistance, pullDuration / maxPullDuration);
//映射拉弓时间到动画
float pullStrength = Mathf.Clamp01(pullDuration / maxPullDuration);
animator.SetFloat("PullStrength", pullStrength);
// 输出射速和射程用于调试
//Debug.Log("Arrow Speed: " + arrowSpeed);
//Debug.Log("Arrow Distance: " + arrowDistance);
}
//右键松开,装填完毕
if (Input.GetMouseButtonUp(1))
{
//记录右键松开
isRightMouseButtonPressed = false;
//装填完毕
Loaded = true;
//改变动画状态变量
animator.SetBool("isLoading", false);
}
}
// 左键点击,进行射击
if (Input.GetMouseButtonDown(0) && Loaded)
{
//空膛状态
Loaded = false;
//播放音频
AudioSource.PlayClipAtPoint(shootSound, Vector3.zero);
// 播放射击动画
animator.speed = 2;
animator.SetBool("isLoading", false);
animator.SetBool("isShooting", true);
//创建箭头实例
GameObject arrowPrefab = Resources.Load<GameObject>("RyuGiKen/Crossbow/Prefabs/Arrow");
if (arrowPrefab != null)
{
// 实例化箭头预设(从Resources文件夹中加载的箭头预设)
GameObject arrow = Instantiate(arrowPrefab);
// 为新创建的箭头对象动态添加ArrowController组件
arrow.AddComponent<ArrowHitMonster>();
// 获取箭头的ArrowController组件,以便后续操作
ArrowHitMonster arrowHitMonster = arrow.GetComponent<ArrowHitMonster>();
// 将箭头的位置设置为firePoint的当前位置
// firePoint应该是发射箭头的起始点,例如弓箭的弦的位置
arrow.transform.position = firePoint.position;
// 设置箭头的旋转,使其朝向当前物体(弓)的正前方
// Quaternion.LookRotation(this.transform.forward) 会将箭头朝向当前物体(弓)的前方方向
arrow.transform.rotation = Quaternion.LookRotation(Camera.main.transform.forward);
// 使用协程让箭矢匀速移动
StartCoroutine(MoveArrow(arrow));
}
else
{
Debug.LogError("Arrow prefab could not be loaded from Resources. Please check the file path.");
}
}
}
// 使用协程实现匀速移动
private IEnumerator MoveArrow(GameObject arrow)
{
float distanceTraveled = 0f;
Vector3 startPosition = arrow.transform.position;
Vector3 direction = arrow.transform.forward;
while (distanceTraveled < arrowDistance) // 假设箭的最大飞行距离是 100 单位
{
float moveDistance = arrowSpeed * Time.deltaTime;
arrow.transform.Translate(direction * moveDistance, Space.World); // 让箭矢匀速前进
distanceTraveled += moveDistance;
yield return null;
}
// 飞行结束后,可以销毁箭矢
Destroy(arrow);
}
}
6. 怪物(靶子)
(1)靶子设置
静态靶:骷髅;动态靶:僵尸
僵尸自动寻路实现:使用Unity提供的NavMeshAgent自动寻路组件,烘培地图,挂载脚本,自动追踪玩家。
(骷髅打金服)
(NavMesh Surface地图烘培)
僵尸行动相关代码如下:
MonsterBehaviour 类
using UnityEngine;
using UnityEngine.AI;
public class MonsterBehaviour : MonoBehaviour
{
public Transform player; // 玩家的位置
public float detectionRange = 15f; // 怪物的追击范围
public float attackRange = 2f; // 怪物的攻击范围
public float moveSpeed = 3.5f; // 怪物移动速度
public float damage = 1; // 怪物攻击伤害
public float knockbackForce = 12f; // 击退力
public float attackInterval = 1f; // 攻击间隔时间
public PlayerMovementController playerMovementController; // 玩家移动组件,用于击退玩家
public PlayerDamageController playerDamageController; // 玩家受到伤害组件
private NavMeshAgent navAgent; // 怪物的 NavMeshAgent 组件
private Animator animator; // 怪物的动画控制器
private float lastAttackTime = 0f; // 上次攻击的时间
void Start()
{
// 获取NavMeshAgent组件
navAgent = GetComponent<NavMeshAgent>();
// 设置 NavMeshAgent 的移动速度
if (navAgent != null)
{
navAgent.speed = moveSpeed;
}
}
void Update()
{
if (player == null) return; // 如果没有指定玩家对象,跳过更新
// 计算玩家与怪物之间的距离
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// 如果玩家在追击范围内,怪物追击玩家
if (distanceToPlayer <= detectionRange)
{
// 设置 NavMeshAgent 的目标为玩家的位置
navAgent.SetDestination(player.position);
// 如果怪物距离玩家很近,并且达到了攻击间隔(1s),进行攻击
if (distanceToPlayer <= attackRange && Time.time - lastAttackTime >= attackInterval)
{
//Debug.Log("怪物和玩家距离:" + distanceToPlayer);
//Debug.Log("Attacked!");
// 更新上次攻击时间
lastAttackTime = Time.time;
// 进行攻击
AttackPlayer();
}
}
}
// 攻击玩家的逻辑
private void AttackPlayer()
{
//Debug.Log("Attacked+!");
// 计算击退方向(怪物到玩家的方向)
Vector3 knockbackDirection = player.position - transform.position;
// 调用玩家移动控制器来处理击退
playerMovementController.PlayerDamaged(knockbackDirection, knockbackForce);
}
}
(2)碰撞检测
箭头进行boxCast检测,若碰撞到父物体标签为monsters的物体(怪物),则怪物死亡。
(boxCast检测效果)
ArrowHitMonster 类
using UnityEngine;
using System.Collections; // 引入协程所需的命名空间
public class ArrowHitMonster : MonoBehaviour
{
public float speed = 30f; // 箭矢速度
public float maxDistance = 100f; // 最大射程
public Transform firePoint; // 箭矢射出点
public Vector3 boxSize = new Vector3(1f, 1f, 1f); // 设置BoxCast的大小(这可以根据需要调整)
private Vector3 startPosition;
void Start()
{
startPosition = transform.position;
}
void Update()
{
// 箭矢飞行方向
Vector3 direction = transform.forward;
// 使用 BoxCast 检测
RaycastHit hit;
// BoxCast的中心点是箭矢当前的位置,检测方向是箭矢的前方(方向),检测的最大距离是箭矢的速度 * deltaTime
if (Physics.BoxCast(transform.position, boxSize, direction, out hit, Quaternion.identity, 30f * Time.deltaTime))
{
// 检测到碰撞,判断是否击中怪物
if (hit.collider.transform.parent != null && hit.collider.transform.parent.CompareTag("monsters"))
{
Debug.Log("Hit Monster!");
// 父物体是 Zombies,调用 ZombieDead 脚本
if (hit.collider.transform.parent.name == "Zombies")
{
//Debug.Log("射中僵尸了!");
ZombieDead zombieDead = hit.collider.transform.GetComponent<ZombieDead>();
if (zombieDead != null)
{
Debug.Log("射中僵尸了!");
zombieDead.OnHit(); // 调用怪物死亡的逻辑
}
}
// 父物体是 Skeletons,调用 SkeletonDead 脚本
if (hit.collider.transform.parent.name == "Skeletons")
{
//Debug.Log("射中小白了!");
SkeletonDead skeletonDead = hit.collider.transform.GetComponent<SkeletonDead>();
if (skeletonDead != null)
{
Debug.Log("射中小白了!");
skeletonDead.OnHit(); // 调用怪物死亡的逻辑
}
}
}
}
}
// 可选:绘制调试用的 BoxCast 范围(便于调试和测试)
void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(transform.position + transform.forward * (speed * Time.deltaTime), boxSize);
}
}
(3)怪物击杀与计分管理
识别怪物所属父物体名称判断怪物种类,击杀调用ScoreManager 类,击杀僵尸+2分,击杀骷髅+1分并增加2支箭。
ZombieDead 类
using UnityEngine;
using System.Collections;
public class ZombieDead : MonoBehaviour
{
public ScoreManager scoreManager; // 引用 ScoreManager 脚本,用于增加分数
public ParticleSystem smokeEffect; // 引入烟雾粒子系统
private bool isHit = false; // 判断怪物是否被击中
private AudioSource audioSource; //音频
public AudioClip deadSound;
public void OnHit()
{
if (isHit) return; // 如果怪物已经被击中,则不再执行任何操作
isHit = true;
//获取挂载音频
audioSource = GetComponent<AudioSource>(); // 获取挂载的 AudioSource
AudioSource.PlayClipAtPoint(deadSound, Vector3.zero); //播放音频
//播放烟雾粒子效果
smokeEffect.transform.position = transform.position; // 将烟雾位置设置为怪物位置
smokeEffect.Play(); // 播放烟雾粒子效果
//僵尸+2分
int scoreToAdd = 2;
// 增加分数
scoreManager.AddScore(scoreToAdd);
// 销毁怪物
Destroy(gameObject, 0.2f);
}
}
SkeletonDead 类
using UnityEngine;
using System.Collections;
public class SkeletonDead : MonoBehaviour
{
public ScoreManager scoreManager; // 引用 ScoreManager 脚本,用于增加分数
public ParticleSystem smokeEffect; // 引入烟雾粒子系统
//引用 ArrowController 脚本,用于修改箭数
public ArrowCountController arrowCountController;
//引用 IfShootArea 脚本,用于修改骷髅击杀数
public IfShootArea ifShootArea;
private bool isHit = false; // 判断怪物是否被击中
private AudioSource audioSource; //音频
public AudioClip deadSound;
public void OnHit()
{
if (isHit) return; // 如果怪物已经被击中,则不再执行任何操作
isHit = true;
//获取挂载音频
audioSource = GetComponent<AudioSource>(); // 获取挂载的 AudioSource
AudioSource.PlayClipAtPoint(deadSound, Vector3.zero); //播放音频
//播放烟雾粒子效果
smokeEffect.transform.position = transform.position; // 将烟雾位置设置为怪物位置
smokeEffect.Play(); // 播放烟雾粒子效果
//小白+1分
int scoreToAdd = 1;
// 增加分数
scoreManager.AddScore(scoreToAdd);
//增加箭数
arrowCountController.changeArrow(2);
//小白击杀+1
ifShootArea.changeDeadCount(1);
// 销毁怪物
Destroy(gameObject, 0.2f);
}
}
ScoreManager 类
using UnityEngine;
using UnityEngine.UI; // 如果使用 Text 组件的话
using TMPro; // 如果使用 TextMeshPro 的话
//分数显示
public class ScoreManager : MonoBehaviour
{
public TMP_Text scoreText; // 如果使用 TextMeshPro,用这个来显示UI
private int score = 0; // 分数变量
void Start()
{
ResetScore();
}
void UpdateScoreDisplay()
{
// 更新 UI 中的文本显示分数
scoreText.text = "Score: " + score.ToString();
}
public void AddScore(int points)
{
score += points;
UpdateScoreDisplay();
}
public void ResetScore()
{
score = 0;
UpdateScoreDisplay();
}
}
(4)粒子效果
创建粒子播放脚本,怪物死亡后绑定的粒子效果传送到死亡位置并开始播放。代码见上面的ZombieDead 类和SkeletonDead 类。
7. 小地图
新建一个摄像机,镜头固定在鸟瞰游戏地图的方向,摄像机设置为玩家模型的子物体,位置随玩家位置变化而变化;使用Render Texture搭载摄像机,显示在游戏画面左上角。
8. 玩家生命值
僵尸靠近玩家会进行攻击,将玩家击退,并扣除一点生命值。
生命值初始有5点,在游戏界面用5颗心表示,生命值清零后,游戏结束。
(UI设计)
代码如下:
PlayerDamageController类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class PlayerDamageController : MonoBehaviour
{
public int maxHealth = 5; // 玩家最大生命值
public int currentHealth = 5; // 当前生命值
public GameObject[] hearts; // 存储心形图标的数组
public TextMeshProUGUI gameOverText; // 死亡文本
void Start()
{
// 初始化心形图标的显示
UpdateHearts();
// 游戏开始时隐藏死亡信息
gameOverText.gameObject.SetActive(false);
}
void Update()
{
// 检查玩家是否死亡
if (currentHealth <= 0 && !gameOverText.gameObject.activeSelf)
{
GameOver(); // 触发游戏结束
}
}
// 处理玩家受伤
public void TakeDamage(int damage)
{
currentHealth -= damage;
if (currentHealth < 0)
{
currentHealth = 0; // 生命值不能小于0
}
UpdateHearts(); // 更新显示的心形图标
}
// 更新心形图标的显示
void UpdateHearts()
{
for (int i = 1; i <= maxHealth; i++)
{
// 显示当前生命值颗心
if (i <= currentHealth)
{
hearts[i-1].SetActive(true); // 显示心形图标
}
else
{
hearts[i-1].SetActive(false); // 隐藏心形图标
}
}
}
// 游戏结束时调用此方法
void GameOver()
{
gameOverText.text = "Wasted"; // 设置显示文本为 "Wasted"
gameOverText.gameObject.SetActive(true); // 显示文本
// 启动协程,在2秒后结束游戏
StartCoroutine(EndGameWithDelay(2f));
}
// 协程:延迟2秒后结束游戏
IEnumerator EndGameWithDelay(float delay)
{
// 等待 2 秒
yield return new WaitForSeconds(2f);
// 退出游戏
Debug.Log("Game Over! You Win!");
Application.Quit(); // 退出游戏
// 在编辑器中退出
UnityEditor.EditorApplication.isPlaying = false; // 仅在编辑器中有效
}
}
9. 音频
导入音频后,使用Audio Source组件进行播放,游戏里搭载有背景音乐,以及拉弓、玩家受伤、怪物死亡等音效。
backgroudmusic类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class backgroudmusic : MonoBehaviour
{
private AudioSource audioSource; //音频
public AudioClip backgroundmusic;
// Start is called before the first frame update
void Start()
{
//获取挂载音频
audioSource = GetComponent<AudioSource>(); // 获取挂载的 AudioSource
AudioSource.PlayClipAtPoint(backgroundmusic, Vector3.zero); //播放音频
}
// Update is called once per frame
void Update()
{
}
}
10. 游戏胜利条件
到达村庄钻石傀儡处,游戏胜利,自动结束。
控制代码如下:
GameControllerScript 类
using UnityEngine;
using TMPro; // 如果使用 TextMeshPro 的话
using UnityEngine.SceneManagement; // 引入场景管理
using System.Collections; // 引入协程所需的命名空间
public class GameControllerScript : MonoBehaviour
{
public Vector3 targetPosition = new Vector3(-34f, 25f, 51f); // 目标位置
public Transform player; // 玩家对象
public TMP_Text win; // 显示胜利文本的UI组件
public float winDistance = 10f; // 到达目标位置的距离阈值
void Start()
{
// 在游戏开始时确保胜利文本不可见
win.enabled = false;
// 隐藏鼠标
Cursor.visible = false;
// 锁定鼠标到屏幕中心
Cursor.lockState = CursorLockMode.Locked;
}
void Update()
{
// 检查玩家与目标位置之间的距离
float distanceToTarget = Vector3.Distance(player.position, targetPosition);
// 如果距离小于设定的阈值,显示胜利消息并结束游戏
if (distanceToTarget <= winDistance)
{
ShowWinMessage();
// 启动协程
if (!isGameOver) // 防止重复启动协程
{
StartCoroutine(EndGameWithDelay());
isGameOver = true;
}
}
}
// 显示胜利文本
void ShowWinMessage()
{
win.enabled = true; // 显示文本
}
private bool isGameOver = false;
// 协程:等待 2 秒后结束游戏
IEnumerator EndGameWithDelay()
{
// 等待 2 秒
yield return new WaitForSeconds(2f);
// 退出游戏
Debug.Log("Game Over! You Win!");
Application.Quit(); // 退出游戏
// 在编辑器中退出
UnityEditor.EditorApplication.isPlaying = false; // 仅在编辑器中有效
}
}