0经验上手手游Unity3D开发

2015年07月24日 14:05 0 点赞 0 评论 更新于 2025-11-21 18:44

在当今的游戏市场中,手游的发展可谓如火如荼,游戏引擎也层出不穷。除了在3D游戏开发领域占据主导地位的Unity3D,以及在2D游戏开发中独树一帜的Cocos2D - X之外,还有虚幻引擎、Sphinx等,甚至搜狐也推出了国产的Genesis - 3D引擎。

一、文章适用人群

本文适合对Unity基础组件有一定了解,想要知道如何在项目中具体应用各种组件的开发者。文章将以Asset Store上的“Unity Projects Stealth”为例,讲解Unity的相关知识,因此读者需要对Unity的一些基本概念有所了解。

二、项目运行环境

在开始之前,先介绍一下本文运行项目时的环境:

  • 系统:Windows 7 X64
  • Unity:4.3.3f1

三、项目大致了解

(一)导入项目

打开Unity,新建项目,然后导入下载的“Unity Projects Stealth.unitypackage”。导入完成后,可通过菜单栏“Window” - “Project”打开“Project”视图,其结构如下: | 目录名称 | 说明 | | ---- | ---- | | Animations | 存放一些角色动画 | | Animator | 动画控制器,当前项目中该目录为空 | | Audio | 音频文件 | | Fonts | 字体文件 | | Gizmos | 用于在Scene视图中调试的图标 | | Materials | 材质文件 | | Models | 模型文件 | | Prefabs | 预设文件 | | Scenes | 场景文件,当前项目中该目录为空 | | Scripts | 脚本文件,当前项目中该目录为空 | | Shaders | 着色器文件 | | Textures | 贴图纹理文件 | | Done | 包含了前面缺少的动画控制器、场景文件和脚本 |

在实际项目开发中,我们可以参考这种做法,将不同类型的资源存放在不同的目录中进行归类整理。这样做不仅可以减少沟通成本,还能方便自己查找资源。

(二)运行项目

确认无误后,点击播放按钮,项目即可运行。使用键盘的WASD键或上下左右箭头可以控制人物走动,屏幕左下方会显示其他操作的按键提示,例如按下“Z”键可以打开开关。

四、碰撞器基础

(一)添加碰撞器

若想让物体具有体积并能产生碰撞,需要为该物体添加碰撞器“Collider”。在Unity中,选择一个对象,然后点击菜单栏的“Component” - “Physics”,其中提供了多种类型的碰撞器,可根据模型的形状选择较为合适的碰撞器。

(二)碰撞器分类

Unity中的碰撞器分为静态碰撞器和动态碰撞器两种:

  • 静态碰撞器:在游戏过程中,该碰撞器不会发生位移、旋转和缩放。
  • 动态碰撞器:在游戏过程中,碰撞器可能会发生位移、旋转和缩放。动态碰撞器又可分为两种:
  • CharacterController
  • 普通碰撞器 + 刚体组件:使用这种组合时,必须添加刚体组件,否则可能会导致碰撞失效或增加性能开销。例如,Unity的UI组件NGUI在新版本中,其Panel会检测当前对象是否带有Rigibody组件,若没有则会自动添加,以避免开发者在制作界面动画时忘记添加Rigibody组件,导致UI按钮点击失效。

五、控制角色

(一)基本控制方法

在场景中控制角色运动,最简单的方法是将一个对象拖入场景,然后根据按键设置该对象Transform的position值,从而使对象在场景中移动。可以使用Input.GetAxis方法获取按键输入,获取X和Z轴的按键分别使用Input.GetAxis("Horizontal")和Input.GetAxis("Vertical")方法,其中“Horizontal”和“Vertical”是在“Edit” - “Project Settings” - “Input”中配置的。

(二)示例项目中的角色控制

在示例项目中,我们在“Hierarchy”视图(若未打开,可通过“Window” - “Hierarchy”打开)中找到“char_ethan”对象并选中它。查看其“Inspector”视图(若未打开,可通过“Window” - “Inspector”打开),除了基本的Transform组件外,还包含“Animator”、“Capsule Collider”、“Rigidbody”、“Audio Source”和“Audio Listener”组件。其中,“Animator”、“Capsule Collider”和“Rigidbody”这三个组件是实现可移动物体的常用组合(当然,也可以使用“Character Controller”),“Audio Source”和“Audio Listener”分别用于播放和监听声音。此外,“Done Player Health (Script)”、“Done Player Inventory (Script)”和“Done Player Movement (Script)”是用于控制角色的脚本。下面对各组件的用途进行简要介绍:

  • Animator:Unity内置的动画控制器,基于状态机原理。双击“Animator”面板中的“Controller”属性,会弹出状态机的编辑窗口。
  • Capsule Collider:胶囊体碰撞器,结构相对简单。
  • Rigidbody:动态碰撞器所需的刚体组件。

(三)控制脚本分析

下面重点分析“DonePlayerMovement”脚本:

using UnityEngine;
using System.Collections;

public class DonePlayerMovement : MonoBehaviour
{
public AudioClip shoutingClip;      // 玩家大喊的声音
public float turnSmoothing = 15f;   // 用于玩家平滑转向的值
public float speedDampTime = 0.1f;  // 用于控制从一个值变化到另一个的时间限制

private Animator anim;
private DoneHashIDs hash;           // 保存各种动画状态的hash

void Awake ()
{
anim = GetComponent<Animator>();
hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();

// Set the weight of the shouting layer to 1.
anim.SetLayerWeight(1, 1f);
}

void FixedUpdate ()
{
// Cache the inputs.
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
bool sneak = Input.GetButton("Sneak");

MovementManagement(h, v, sneak);
}

void Update ()
{
// Cache the attention attracting input.
bool shout = Input.GetButtonDown("Attract");

// Set the animator shouting parameter.
anim.SetBool(hash.shoutingBool, shout);

AudioManagement(shout);
}

void MovementManagement (float horizontal, float vertical, bool sneaking)
{
// Set the sneaking parameter to the sneak input.
anim.SetBool(hash.sneakingBool, sneaking);

// If there is some axis input...
if(horizontal != 0f || vertical != 0f)
{
// ... set the players rotation and set the speed parameter to 5.5f.
Rotating(horizontal, vertical);
anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);
}
else
// Otherwise set the speed parameter to 0.
anim.SetFloat(hash.speedFloat, 0);
}

void Rotating (float horizontal, float vertical)
{
// Create a new vector of the horizontal and vertical inputs.
Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);

// Create a rotation based on this new vector assuming that up is the global y axis.
Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);

// Create a rotation that is an increment closer to the target rotation from the player's rotation.
Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);

// Change the players rotation to this new rotation.
rigidbody.MoveRotation(newRotation);
}

void AudioManagement (bool shout)
{
// If the player is currently in the run state...
if(anim.GetCurrentAnimatorStateInfo(0).nameHash == hash.locomotionState)
{
// ... and if the footsteps are not playing...
if(!audio.isPlaying)
// ... play them.
audio.Play();
}
else
// Otherwise stop the footsteps.
audio.Stop();

// If the shout input has been pressed...
if(shout)
// ... play the shouting clip where we are.
AudioSource.PlayClipAtPoint(shoutingClip, transform.position);
}
}

该脚本的主要逻辑如下:

  • Awake方法:在脚本初始化时,查找并缓存Animator组件和DoneHashIds组件,避免后续频繁查找,节省CPU资源。
  • FixedUpdate方法:获取玩家按下的移动键,处理移动和角色朝向问题。
  • Update方法:检测玩家是否按下“Attract”键,确定是否播放动画和声音。

下面详细分析MovementManagement方法:

void MovementManagement (float horizontal, float vertical, bool sneaking)
{
// Set the sneaking parameter to the sneak input.
anim.SetBool(hash.sneakingBool, sneaking);

// If there is some axis input...
if(horizontal != 0f || vertical != 0f)
{
// ... set the players rotation and set the speed parameter to 5.5f.
Rotating(horizontal, vertical);
anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);
}
else
// Otherwise set the speed parameter to 0.
anim.SetFloat(hash.speedFloat, 0);
}

该方法主要根据玩家的按键输入设置角色的移动状态。若有按键输入,则调用Rotating方法调整角色朝向,并设置动画的速度参数为5.5f;若没有按键输入,则将速度参数设置为0。

Rotating方法的代码如下:

void Rotating (float horizontal, float vertical)
{
// Create a new vector of the horizontal and vertical inputs.
Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);

// Create a rotation based on this new vector assuming that up is the global y axis.
Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);

// Create a rotation that is an increment closer to the target rotation from the player's rotation.
Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);

// Change the players rotation to this new rotation.
rigidbody.MoveRotation(newRotation);
}

该方法根据玩家的按键输入计算目标方向,然后通过四元数插值计算新的旋转角度,并更新角色的旋转状态。

在代码中,虽然没有直接实现玩家位置移动的代码,但角色却能在场景中移动。这是因为在“Hierarchy”视图中的“char_ethan”对象的Animator组件勾选了“Apply Root Motion”选项。根据官方文档的解释,Root motion是指对象的整个网格从起始点移动的效果,这种移动是由动画本身产生的,而不是通过改变Transform的位置实现的。也就是说,角色的移动是在动画中实现的,通过勾选Animator的ApplyRootMotion选项来启用。

关于角色控制中anim.SetFloat方法涉及的动画混合,本文暂不详细介绍。

六、碰撞器及其使用

(一)碰撞器类型回顾

前面提到,Unity中的碰撞器分为静态和动态两种。在示例项目中,角色“char_ethan”使用的是动态碰撞器,具体为“Capsule Collider”组件和“rigidbody”组件的组合,即普通碰撞器 + 刚体组件。

(二)碰撞器的重要选项

碰撞器有一个重要的选项“Is Trigger”:

  • 若两个相互碰撞的碰撞器中有任何一个勾选了IsTrigger,则会触发这两个碰撞器上的OnTriggerEnter方法。
  • 若两个相互碰撞的碰撞器都未勾选IsTrigger,则会触发这两个碰撞器上的OnCollisionEnter、OnCollisionStay和OnCollisionExit方法。

需要注意的是,只有在不勾选IsTrigger时,才能使用Physics.Raycast进行射线检测。因此,当碰撞方法未被调用时,需要确保IsTrigger选项和方法的设置是匹配的。在本游戏中,玩家走路时不会穿墙,是因为墙的碰撞器和玩家身上的碰撞器都未勾选IsTrigger。

关于各种内置碰撞器的详细知识,可参考Unity官方文档:http://docs.unity3d.com/Manual/Physics3DReference.html

七、怪物AI

在“Hierarchy”视图中,有三个怪物对象“char_robotGuard_001”、“char_robotGuard_002”和“char_robotGuard_003”,它们的组件设置基本相同,下面以“char_robotGuard_001”为例进行分析。

(一)怪物组件分析

  • Animator:与角色控制中的Animator组件类似,用于控制怪物的动画。
  • 碰撞器:怪物身上有两个碰撞器,一个是未勾选IsTrigger的胶囊体碰撞器,用于防止角色穿过建筑物;另一个是勾选了IsTrigger的球体碰撞器,其作用将在后续脚本中体现。

(二)DoneEnemySight组件

using UnityEngine;
using System.Collections;

public class DoneEnemySight : MonoBehaviour
{
// Number of degrees, centred on forward, for the enemy see.
public float fieldOfViewAngle = 110f;
// Whether or not the player is currently sighted.
public bool playerInSight;
// Last place this enemy spotted the player.
public Vector3 personalLastSighting;

// Reference to the NavMeshAgent component.
private NavMeshAgent nav;
// Reference to the sphere collider trigger component.
private SphereCollider col;
// Reference to the Animator.
private Animator anim;
// Reference to last global sighting of the player.
private DoneLastPlayerSighting lastPlayerSighting;
// Reference to the player.
private GameObject player;
// Reference to the player's animator component.
private Animator playerAnim;
// Reference to the player's health script.
private DonePlayerHealth playerHealth;
// Reference to the HashIDs.
private DoneHashIDs hash;
// Where the player was sighted last frame.
private Vector3 previousSighting;

void Awake ()
{
// Setting up the references.
nav = GetComponent<NavMeshAgent>();
col = GetComponent<SphereCollider>();
anim = GetComponent<Animator>();
lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();
player = GameObject.FindGameObjectWithTag(DoneTags.player);
playerAnim = player.GetComponent<Animator>();
playerHealth = player.GetComponent<DonePlayerHealth>();
hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();

// Set the personal sighting and the previous sighting to the reset position.
personalLastSighting = lastPlayerSighting.resetPosition;
previousSighting = lastPlayerSighting.resetPosition;
}

void Update ()
{
// If the last global sighting of the player has changed...
if(lastPlayerSighting.position != previousSighting)
// ... then update the personal sighting to be the same as the global sighting.
personalLastSighting = lastPlayerSighting.position;

// Set the previous sighting to the be the sighting from this frame.
previousSighting = lastPlayerSighting.position;

// If the player is alive...
if(playerHealth.health > 0f)
// ... set the animator parameter to whether the player is in sight or not.
anim.SetBool(hash.playerInSightBool, playerInSight);
else
// ... set the animator parameter to false.
anim.SetBool(hash.playerInSightBool, false);
}

void OnTriggerStay (Collider other)
{
// If the player has entered the trigger sphere...
if(other.gameObject == player)
{
// By default the player is not in sight.
playerInSight = false;

// Create a vector from the enemy to the player and store the angle between it and forward.
Vector3 direction = other.transform.position - transform.position;
float angle = Vector3.Angle(direction, transform.forward);

// If the angle between forward and where the player is, is less than half the angle of view...
if(angle < fieldOfViewAngle * 0.5f)
{
RaycastHit hit;

// ... and if a raycast towards the player hits something...
if(Physics.Raycast(transform.position + transform.up, direction.normalized, out hit, col.radius))
{
// ... and if the raycast hits the player...
if(hit.collider.gameObject == player)
{
// ... the player is in sight.
playerInSight = true;

// Set the last global sighting is the players current position.
lastPlayerSighting.position = player.transform.position;
}
}
}

// Store the name hashes of the current states.
int playerLayerZeroStateHash = playerAnim.GetCurrentAnimatorStateInfo(0).nameHash;
int playerLayerOneStateHash = playerAnim.GetCurrentAnimatorStateInfo(1).nameHash;

// If the player is running or is attracting attention...
if(playerLayerZeroStateHash == hash.locomotionState || playerLayerOneStateHash == hash.shoutState)
{
// ... and if the player is within hearing range...
if(CalculatePathLength(player.transform.position) <= col.radius)
// ... set the last personal sighting of the player to the player's current position.
personalLastSighting = player.transform.position;
}
}
}

void OnTriggerExit (Collider other)
{
// If the player leaves the trigger zone...
if(other.gameObject == player)
// ... the player is not in sight.
playerInSight = false;
}

float CalculatePathLength (Vector3 targetPosition)
{
// Create a path and set it based on a target position.
NavMeshPath path = new NavMeshPath();
if(nav.enabled)
nav.CalculatePath(targetPosition, path);

// Create an array of points which is the length of the number of corners in the path + 2.
Vector3 [] allWayPoints = new Vector3[path.corners.Length + 2];

// The first point is the enemy's position.
allWayPoints[0] = transform.position;

// The last point is the target position.
allWayPoints[allWayPoints.Length - 1] = targetPosition;

// The points inbetween are the corners of the path.
for(int i = 0; i < path.corners.Length; i++)
{
allWayPoints[i + 1] = path.corners[i];
}

// Create a float to store the path length that is by default 0.
float pathLength = 0;

// Increment the path length by an amount equal to the distance between each waypoint and the next.
for(int i = 0; i < allWayPoints.Length - 1; i++)
{
pathLength += Vector3.Distance(allWayPoints[i], allWayPoints[i + 1]);
}

return pathLength;
}
}

该组件的主要功能是检测玩家是否在怪物的视野内,并设置玩家的位置信息和是否被发现的信息,各方法的作用如下:

  • Awake方法:获取所需的组件引用,并初始化玩家的位置信息。
  • Update方法:检查玩家最后一次被发现的位置是否发生变化,同步位置信息,并将玩家是否被发现的状态设置到Animator的状态机中。
  • OnTriggerStay方法:当玩家进入怪物的视野(球体碰撞器)时触发,检查玩家是否在怪物前方且中间没有遮挡物,若满足条件则设置playerInSight为true,并更新玩家的位置信息。同时,若玩家处于跑步或吸引注意力的状态,且在怪物的听力范围内,也会更新玩家的位置信息。
  • OnTriggerExit方法:当玩家离开怪物的视野时,设置playerInSight为false。
  • CalculatePathLength方法:计算怪物到玩家的路径长度。

(三)DoneEnemyAI组件

using UnityEngine;
using System.Collections;

public class DoneEnemyAI : MonoBehaviour
{
public float patrolSpeed = 2f;
// The nav mesh agent's speed when chasing.
public float chaseSpeed = 5f;
// The amount of time to wait when the last sighting is reached.
public float chaseWaitTime = 5f;
// The amount of time to wait when the patrol way point is reached.
public float patrolWaitTime = 1f;
// An array of transforms for the patrol route.
public Transform[] patrolWayPoints;

// Reference to the EnemySight script.
private DoneEnemySight enemySight;
// Reference to the nav mesh agent.
private NavMeshAgent nav;
// Reference to the player's transform.
private Transform player;
// Reference to the PlayerHealth script.
private DonePlayerHealth playerHealth;
// Reference to the last global sighting of the player.
private DoneLastPlayerSighting lastPlayerSighting;
// A timer for the chaseWaitTime.
private float chaseTimer;
// A timer for the patrolWaitTime.
private float patrolTimer;
// A counter for the way point array.
private int wayPointIndex;

void Awake ()
{
// Setting up the references.
enemySight = GetComponent<DoneEnemySight>();
nav = GetComponent<NavMeshAgent>();
player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;
playerHealth = player.GetComponent<DonePlayerHealth>();
lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();
}

void Update ()
{
// If the player is in sight and is alive...
if(enemySight.playerInSight && playerHealth.health > 0f)
// ... shoot.
Shooting();

// If the player has been sighted and isn't dead...
else if(enemySight.personalLastSighting != lastPlayerSighting.resetPosition && playerHealth.health > 0f)
// ... chase.
Chasing();

// Otherwise...
else
// ... patrol.
Patrolling();
}

void Shooting ()
{
// Stop the enemy where it is.
nav.Stop();
}

void Chasing ()
{
// Create a vector from the enemy to the last sighting of the player.
Vector3 sightingDeltaPos = enemySight.personalLastSighting - transform.position;

// If the the last personal sighting of the player is not close...
if(sightingDeltaPos.sqrMagnitude > 4f)
// ... set the destination for the NavMeshAgent to the last personal sighting of the player.
nav.destination = enemySight.personalLastSighting;

// Set the appropriate speed for the NavMeshAgent.
nav.speed = chaseSpeed;

// If near the last personal sighting...
if(nav.remainingDistance < nav.stoppingDistance)
{
// ... increment the timer.
chaseTimer += Time.deltaTime;

// If the timer exceeds the wait time...
if(chaseTimer >= chaseWaitTime)
{
// ... reset last global sighting, the last personal sighting and the timer.
lastPlayerSighting.position = lastPlayerSighting.resetPosition;
enemySight.personalLastSighting = lastPlayerSighting.resetPosition;
chaseTimer = 0f;
}
}
else
// If not near the last sighting personal sighting of the player, reset the timer.
chaseTimer = 0f;
}

void Patrolling ()
{
// Set an appropriate speed for the NavMeshAgent.
nav.speed = patrolSpeed;

// If near the next waypoint or there is no destination...
if(nav.destination == lastPlayerSighting.resetPosition || nav.remainingDistance < nav.stoppingDistance)
{
// ... increment the timer.
patrolTimer += Time.deltaTime;

// If the timer exceeds the wait time...
if(patrolTimer >= patrolWaitTime)
{
// ... increment the wayPointIndex.
if(wayPointIndex == patrolWayPoints.Length - 1)
wayPointIndex = 0;
else
wayPointIndex++;

// Reset the timer.
patrolTimer = 0;
}
}
else
// If not near a destination, reset the timer.
patrolTimer = 0;

// Set the destination to the patrolWayPoint.
nav.destination = patrolWayPoints[wayPointIndex].position;
}
}

该组件的主要功能是根据玩家的状态控制怪物的行为,具体逻辑如下:

  • Awake方法:获取所需的组件引用。
  • Update方法:根据玩家是否在视野内、是否存活以及最后一次被发现的位置,决定怪物是进行射击、追击还是巡逻。
  • Shooting方法:停止怪物的移动。
  • Chasing方法:设置怪物的移动目标为玩家最后一次被发现的位置,调整移动速度为追击速度。若接近目标位置,启动计时器,当计时器超过等待时间时,重置玩家的位置信息和计时器。
  • Patrolling方法:设置怪物的巡逻速度,若接近巡逻点或没有目标位置,启动计时器,当计时器超过等待时间时,更新巡逻点的索引,并重置计时器。

(四)DoneEnemyShooting组件

using UnityEngine;
using System.Collections;

public class DoneEnemyShooting : MonoBehaviour
{
// The maximum potential damage per shot.
public float maximumDamage = 120f;
// The minimum potential damage per shot.
public float minimumDamage = 45f;
// An audio clip to play when a shot happens.
public AudioClip shotClip;
// The intensity of the light when the shot happens.
public float flashIntensity = 3f;
// How fast the light will fade after the shot.
public float fadeSpeed = 10f;

// Reference to the animator.
private Animator anim;
// Reference to the HashIDs script.
private DoneHashIDs hash;
// Reference to the laser shot line renderer.
private LineRenderer laserShotLine;
// Reference to the laser shot light.
private Light laserShotLight;
// Reference to the sphere collider.
private SphereCollider col;
// Reference to the player's transform.
private Transform player;
// Reference to the player's health.
private DonePlayerHealth playerHealth;
// A bool to say whether or not the enemy is currently shooting.
private bool shooting;
// Amount of damage that is scaled by the distance from the player.
private float scaledDamage;

void Awake ()
{
// Setting up the references.
anim = GetComponent<Animator>();
laserShotLine = GetComponentInChildren<LineRenderer>();
laserShotLight = laserShotLine.gameObject.light;
col = GetComponent<SphereCollider>();
player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;
playerHealth = player.gameObject.GetComponent<DonePlayerHealth>();
hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();

// The line renderer and light are off to start.
laserShotLine.enabled = false;
laserShotLight.intensity = 0f;

// The scaledDamage is the difference between the maximum and the minimum damage.
scaledDamage = maximumDamage - minimumDamage;
}

void Update ()
{
// Cache the current value of the shot curve.
float shot = anim.GetFloat(hash.shotFloat);

// If the shot curve is peaking and the enemy is not currently shooting...
if(shot > 0.5f && !shooting)
// ... shoot
Shoot();

// If the shot curve is no longer peaking...
if(shot < 0.5f)
{
// ... the enemy is no longer shooting and disable the line renderer.
shooting = false;
laserShotLine.enabled = false;
}

// Fade the light out.
laserShotLight.intensity = Mathf.Lerp(laserShotLight.intensity, 0f, fadeSpeed * Time.deltaTime);
}

void OnAnimatorIK (int layerIndex)
{
// Cache the current value of the AimWeight curve.
float aimWeight = anim.GetFloat(hash.aimWeightFloat);

// Set the IK position of the right hand to the player's centre.
anim.SetIKPosition(AvatarIKGoal.RightHand, player.position + Vector3.up * 1.5f);

// Set the weight of the IK compared to animation to that of the curve.
anim.SetIKPositionWeight(AvatarIKGoal.RightHand, aimWeight);
}

void Shoot ()
{
// The enemy is shooting.
shooting = true;

// The fractional distance from the player, 1 is next to the player, 0 is the player is at the extent of the sphere collider.
float fractionalDistance = (col.radius - Vector3.Distance(transform.position, player.position)) / col.radius;

// The damage is the scaled damage, scaled by the fractional distance, plus the minimum damage.
float damage = scaledDamage * fractionalDistance + minimumDamage;

// The player takes damage.
playerHealth.TakeDamage(damage);

// Display the shot effects.
ShotEffects();
}

void ShotEffects ()
{
// Set the initial position of the line renderer to the position of the muzzle.
laserShotLine.SetPosition(0, laserShotLine.transform.position);

// Set the end position of the player's centre of mass.
laserShotLine.SetPosition(1, player.position + Vector3.up * 1.5f);

// Turn on the line renderer.
laserShotLine.enabled = true;

// Make the light flash.
laserShotLight.intensity = flashIntensity;

// Play the gun shot clip at the position of the muzzle flare.
AudioSource.PlayClipAtPoint(shotClip, laserShotLight.transform.position);
}
}

该组件的主要功能是实现怪物的射击逻辑,具体如下:

  • Awake方法:获取所需的组件引用,初始化射击效果的相关参数。
  • Update方法:检查动画的shotFloat属性,判断是否应该射击或停止射击。若射击曲线达到峰值且怪物未射击,则调用Shoot方法;若射击曲线低于阈值,则停止射击并禁用激光线渲染器。同时,逐渐淡化射击光效。
  • OnAnimatorIK方法:根据动画的AimWeight曲线设置怪物右手的IK位置,使其指向玩家。
  • Shoot方法:设置怪物正在射击的状态,计算射击的伤害值,调用玩家的TakeDamage方法使玩家受到伤害,并显示射击效果。
  • ShotEffects方法:设置激光线的起始和结束位置,启用激光线渲染器,使射击光效闪烁,并播放射击音效。

八、事件通知

在本游戏中,除了怪物的AI,还有用于监控玩家的摄像头和红外线墙,它们的原理与怪物的视野检测类似,都是通过监听碰撞器的OnTriggerStay方法来设置玩家被发现的全局位置(DoneLastPlayerSighting)。

然而,这种实现方式存在一定的问题。各个组件都直接获取DoneLastPlayerSighting并检查和设置其状态,这会导致程序变量跟踪困难。为了提高代码的可维护性和清晰度,可以将DoneLastPlayerSighting设计为一个消息中心,每个敌人订阅该消息中心的消息(玩家被发现的位置发生变化的消息),消息中心负责通知所有订阅的对象。订阅的对象也可以将自己发现的目标提交给消息中心,由消息中心确认后通知其他订阅对象。

九、渲染特效

(一)雾效

在Unity中,可通过点击菜单栏的“Edit” - “Render Settings”,在右边的Inspector面板中勾选“fog”选项来开启雾效。同时,可通过“Fog Color”调整雾的颜色。

(二)监视器投射的亮点

在“Hierarchy”视图中找到监视器物体“prop_cctvCam_001”,其下的子物体“cam_frustum_collision”上有一个Light组件,Light的Cookie属性对应的贴图用于产生监视器投射到地上的亮点。

(三)激光墙的激光

激光墙的激光使用了“Self - Illumin/Diffuse”着色器,这是一种自发光着色器。在相同的灯光条件下,使用自发光着色器的物体比使用Diffuse着色器的物体更亮。

作者信息

洞悉

洞悉

共发布了 3994 篇文章