当前位置: 首页 > news >正文

Unity开发一个FPS游戏之三

在前面的两篇博客中,我已实现了一个FPS游戏的大部分功能,包括了第一人称的主角运动控制,武器射击以及敌人的智能行为。这里我将继续完善这个游戏,包括以下几个方面:

  1. 增加一个真实的游戏场景,模拟一个废弃的工厂环境
  2. 完善NPC的智能行为,增加巡逻,警戒,躲避,战术编队等行为。
  3. 增加子弹与场景物体交互的效果。
  4. 游戏道具的拾取。
  5. 完善UI界面,增加场景的任务描述,任务完成或失败的判定展示。

以下是完成后的演示。

mission_critical

游戏场景设计

这是一个很大的课题,暂时不是我关注的重点,为此我先直接采用Unity商店的一个免费的资源,RPG/FPS Game Assets for PC/Mobile (Industrial Set v2.0) | 3D 工业场景 | Unity Asset Store。

NPC智能行为

在之前的博客,已经实现了NPC的部分智能行为,包括了随机搜索以及跟踪玩家的行为。在这里,我将添加巡逻,警戒,躲藏、战术编队等行为。

巡逻警戒

首先是实现巡逻功能,这个可以通过定义巡逻路线经过的关键点,然后利用Unity自带的寻路功能来实现。为此,我们需要对场景的道路进行烘培,点击Window->AI->Navigation,然后选择Bake选项,对场景的可导航路线进行烘培。烘培完成后,需要给Enemy这个预制件增加一个NavMesh Agent的组件,然后在代码中就可以指定目标点的坐标,让Agent自己导航到目的地。

其次是增加警戒模式,这个模式比较简单,就是传入一个警戒点的坐标,然后敌人就在警戒点不定期的随机搜索四周。以下是对WanderingAI的代码进行修改,增加巡逻和警戒这两个行为:

[Header("Enemy behavior")]
...
public bool patrolBehavior = false;
public Vector3[] patrolPoints = null;
public bool guardBehavior = false;
public float guardAngle = 60f;
public float guardMaxRotatePeriod = 10.0f;
public float initialGuardAngle = 0f;private long _guardTS = 0;
private float _guardChangeTime = 0;[Flags]
private enum EnemyStatus {  ...Patrol,Guard
}void Start()
{_animator = GetComponent<Animator>();muzzleEffect = GetComponent<MuzzleEffect>();_agent = GetComponent<NavMeshAgent>();currentAmmo = ammo;if (patrolBehavior && patrolPoints.Length >= 2) {status = EnemyStatus.Patrol;_agent.autoBraking = false;_agent.destination = patrolPoints[destPoint];transform.forward = patrolPoints[destPoint] - patrolPoints[0];_animator.SetTrigger("E_Walk");} else if (guardBehavior) {status = EnemyStatus.Guard;transform.localEulerAngles = new Vector3(0f, initialGuardAngle, 0f);_guardTS = DateTime.UtcNow.Ticks;_guardChangeTime = UnityEngine.Random.Range(1.0f, guardMaxRotatePeriod);_animator.SetTrigger("E_Guard");} else {status = EnemyStatus.Idle;}
}// Update is called once per frame
void Update()
{        if (status == EnemyStatus.AimLeft || status == EnemyStatus.AimRight) {AimMove();} else if (status == EnemyStatus.Reload || status == EnemyStatus.Death) {return;} else if (status == EnemyStatus.Aim) {TurnToPlayer();} else if (status == EnemyStatus.Hide) {Hide();} else if (status == EnemyStatus.Damage) {TurnToDamage();if (!_animator.GetBool("E_IsDamage")) {status = EnemyStatus.Idle;prevPlayerPosition = new Vector3(100f, 100f, 100f);}} else {if (status == EnemyStatus.Walk) {Walk();}if (status == EnemyStatus.Sprint) {Sprint();}if (status == EnemyStatus.Patrol) {Patrol();}if (status == EnemyStatus.Guard) {Guard();}DetectPlayer();}
}void Patrol() {if (!_agent.pathPending && _agent.remainingDistance < 0.5f) {Vector3 prevPosition = patrolPoints[destPoint];destPoint = (destPoint + 1) % patrolPoints.Length;_agent.destination = patrolPoints[destPoint];transform.forward = patrolPoints[destPoint] - prevPosition;}
}public void SetPatrolPoints(Vector3[] points) {patrolPoints = points;
}private void Guard() {float interval = (DateTime.UtcNow.Ticks - _guardTS)/10000000.0f;if (interval >= _guardChangeTime) {_guardTS = DateTime.UtcNow.Ticks;_guardChangeTime = UnityEngine.Random.Range(1.0f, guardMaxRotatePeriod);float rotateAngle = UnityEngine.Random.Range(-guardAngle, guardAngle);transform.localEulerAngles = new Vector3(0f, initialGuardAngle + rotateAngle, 0f);}
}

从以上代码可以看到,要设定敌人的行为是巡逻,需要传入巡逻点的坐标,然后依次把巡逻点的坐标设置为Agent的目的地即可,在巡逻的过程中,敌人同样需要检测玩家,如果发现玩家则改变为瞄准行为,这个和之前的随机搜索的行为切换是保持一致的。当行为是警戒时,将随机不定时的转动一些角度来探测玩家。

装弹躲藏

进一步改进敌人在交战时的行为,当敌人打完子弹时,现在的行为是站定了换子弹,为了提高难度,我们可以设计当敌人打完子弹,自动跑到一些隐藏点来重新装弹,之后再出来和玩家交战。为此我们需要设计一些隐藏点。我采取的方法是在场景中选取一些隐藏点,在每个地点放置一个小的正方体Game Object,然后在游戏装载场景时,读取这些Game Object的坐标,然后把这些Object都设置为SetActive(false)进行隐藏,把这些坐标传给敌人,然后当需要跑去隐藏点时,判断距离最近并且不被玩家发现的隐藏点并自动导航。代码改动如下:

private Vector3[] _hidePoints;private enum EnemyStatus {  ...Hide
}public void SetHidePoints(List<Vector3> points) {_hidePoints = points.ToArray();
}private void Shoot() {if (currentAmmo==0) {Hide();return;}...
}private Vector3 FindNearestHidePoint() {GameObject player = GameObject.Find("Player");Vector3 playerPosition = player.transform.position;float distance = 0f;Vector3 selectedPosition = Vector3.up;RaycastHit info = new RaycastHit();int layermask = LayerMask.GetMask("Character", "Default");for (int i=0;i<_hidePoints.Length;i++) {Vector3 hidePosition = _hidePoints[i];float tempDistance = (transform.position - hidePosition).magnitude;if (tempDistance > eyeviewDistance) {continue;} else {float playerDistance = (playerPosition - hidePosition).magnitude;if (tempDistance > playerDistance) {continue;} else {Physics.Raycast(hidePosition+Vector3.up, playerPosition - hidePosition - Vector3.up, out info, eyeviewDistance, layermask);if (info.collider.name == "Player") {continue;} else {if (distance == 0 || tempDistance < distance) {distance = tempDistance;selectedPosition = hidePosition;}}}}}return selectedPosition;
}private void Hide() {if (status == EnemyStatus.Hide) {if (!_agent.pathPending && _agent.remainingDistance < 0.1f) {_agent.isStopped = true;if (currentAmmo == 0) {status = EnemyStatus.Reload;corReload = StartCoroutine(Reload());}} else {return;}} else {Vector3 hidePoint = FindNearestHidePoint();if (hidePoint != Vector3.up) {transform.forward = hidePoint - transform.position;_agent.isStopped = false;_agent.destination = hidePoint;_agent.speed = sprintSpeed;status = EnemyStatus.Hide;_animator.SetTrigger("E_Sprint");} else {status = EnemyStatus.Reload;corReload = StartCoroutine(Reload());}}
}

听觉识别

当射击开火时,可以设置在某个范围内的敌人会识别声音的方向,并跑到交火的位置附近进行增援。

修改WanderingAI的代码,增加一个方法

public void HearShoot(Vector3 position) {if (status == EnemyStatus.Walk || status == EnemyStatus.Idle || status == EnemyStatus.Patrol) {status = EnemyStatus.Sprint;_agent.destination = position;_agent.isStopped = false;_animator.SetTrigger("E_Sprint");_agent.updateRotation = true;}
}

然后当玩家开火时,我们可以检测在开火的某个范围内,是否有敌人存在,如果有,则调用敌人的WanderingAI的HearShoot方法,把玩家当前的坐标通知给敌人,如果敌人当前处在空闲,随机搜索或巡逻状态时,则会切换为快跑状态,快速跑到玩家位置进行攻击。

修改玩家的PlayerController的代码,在Shoot函数中增加以下部分:

[Header("Notify enemy")]
public float soundDistance = 30.0f;private void Shoot() {if (_input.shoot && _currentBulletVolume>0) {if (!_animator.GetBool("IsReload")) {long nowTS = DateTime.UtcNow.Ticks;float shootInterval = (nowTS - _input.shootPressTS)/10000000.0f;if (shootInterval >= 0.08 || _input.firstShoot) {_animator.SetTrigger("Shoot");bullet = Instantiate(bulletPrefab, _muzzle.position, _muzzle.rotation);bullet.GetComponent<Rigidbody>().velocity = _muzzle.forward * 800.0f;casing = Instantiate(casingPrefab, _eject.position, _eject.rotation);muzzleEffect.Effect(_muzzle.position);_input.shootPressTS = nowTS;_input.firstShoot = false;_currentBulletVolume--;ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.ArmoMessage(_currentBulletVolume));// Simulate the enemy can hear the shoot sound_spottedEnemies = Physics.OverlapSphere(transform.position, soundDistance, LayerMask.GetMask("Enemy"));for (int i=0;i<_spottedEnemies.Length;i++) {WanderingAI behavior = _spottedEnemies[i].gameObject.GetComponent<WanderingAI>();behavior.HearShoot(transform.position);}}}  }
}

战术组队

考虑最简单的一种三角形编队,即领队走在最前面,后面另外两个人和领队呈三角形分布。当发现玩家时,即采取包抄的战术。

对WanderingAI代码改动一下,增加和编队相关的几个属性:

[Header("Enemy Role")]
public bool formation = false;
public int formationType = 0;  // formation type 0: triangle
public int roleId = 0; // role 0: leader, 1: left follower, 2: right follower
public float followDistance = 5.0f;  // How far the follower behind the leaderprivate GameObject _leader;
private Vector3 _adjustFormationDistance;
private List<GameObject> _teamMembers = new List<GameObject>();public void SetLeader(GameObject leader) {_leader = leader;
}public void AddMember(GameObject member) {_teamMembers.Add(member);
}

如果设置了formation为true,那么即表示这个NPC处于编队状态,formationType控制不同的编队类型,当前只实现三角形编队,roleID为0表示这个NPC是领队,领队初始行为是随机搜索,其他roleID的NPC是队员,需要跟着领队,因此设置了一个followDistance来设置其跟随领队的距离。_adjustFormationDistance用于根据followDistance来生成一个Vector3向量,进行距离调整。如果NPC是领队,那么需要保存队员的GameObject到一个list中,如果NPC是队员,那么需要保存领队的GameObject,这样可以方便互相之间的通讯。

增加一个新的状态值,用于表示队员NPC是否处于跟随队长的状态:

[Flags]
private enum EnemyStatus {  ...FollowLeader
}

在Start函数中,对formation状态进行初始化设置

if (formation) {status = EnemyStatus.Walk;_animator.SetTrigger("E_Walk");_adjustFormationDistance = new Vector3(followDistance, 0f, 0f);
}

初始设置编队时,我们可以通过初始化NPC的坐标来保持三角形编队,但是当后续游戏进行时,如果队员遇到障碍物时,需要避让障碍物之后仍能保持队形。我采取一个简便的方法来解决这个问题,那就是让队员遇到障碍物时去到队长当前的位置,而不是随机转向,之后再重新调整队员位置,使得能维持队形。

重新修改Walk方法,并新增一个FollowLeader的方法,代码如下:

void Walk() {if (sprintTS != 0) {long nowTS = DateTime.UtcNow.Ticks;float interval = (nowTS - sprintTS)/10000000.0f;if (interval >= sprintSearchPeriod) {sprintTS = 0;}}float distance = DetectObstacle(transform.forward);if (distance < obstacleRange) {// For NPC, if not the leader, should not random rotate, need to follow to the current leader position.if (formation && roleId != 0 && _leader) {_agent.destination = _leader.transform.position;_agent.isStopped = false;status = EnemyStatus.FollowLeader;return;} else {float angle;if (sprintTS == 0) {angle = UnityEngine.Random.Range(-randomAngle, randomAngle);} else {angle = UnityEngine.Random.Range(-sprintSearchAngle, sprintSearchAngle);}transform.Rotate(0, angle, 0);}}if (formation && roleId != 0 && _leader) {float angle = Vector3.Angle(_leader.transform.forward, _leader.transform.position - transform.position);if (angle >= 30) {transform.forward = _leader.transform.forward;}}transform.Translate(0, 0, speed * Time.deltaTime);
}void FollowLeader() {if (_agent.remainingDistance < 0.5f) {_agent.isStopped = true;randomSearch = true;_animator.SetTrigger("E_Walk");status = EnemyStatus.Walk;// ReformationVector3 direction;if (roleId == 1) {direction = _leader.transform.position - _adjustFormationDistance - transform.position;} else {direction = _leader.transform.position + _adjustFormationDistance - transform.position;}transform.forward = direction;} else {FaceTarget();}
}

当队员遇到障碍时,设置状态为FollowLeader,并且通过NavmeshAgent自动导航到队长当前位置。当处于FollowLeader状态时,如果接近队长位置,则重新调整方向,设置状态为Walk,然后在Walk方法中判断队员与队长的方向的夹角和距离,如果满足条件则重新调整方向,使得重新维持队形不变。

最后就是考虑到当队长NPC挂掉时,要通知队员解散编队,为此定义一个Deformation方法

public void DeFormation() {foreach (GameObject _member in _teamMembers) {if (_member) {WanderingAI _behavior = _member.GetComponent<WanderingAI>();_behavior.formation = false;_behavior.roleId = 0;}}
}

考虑到当前游戏场景的位置比较狭小,不方便队员在发现玩家时进行包抄,所以暂时不添加更多的战术功能,只实现一个编队搜索。

子弹与场景物体的交互

为了使游戏更真实,我们可以让子弹在射击到水泥墙面或者金属物体时产生碰撞效果和留下弹痕,当射击到油罐之类的物体时,可以引爆油罐。

下面来实现子弹撞击物体的效果。

模拟灰尘溅射

首先是模拟子弹撞击水泥物体之后产生的灰尘溅射。在Asset的Texture目录下导入一张灰尘的图片,设置里面要勾选Alpha is Transparancy。例如下图:

在Asset的Material目录下,新建一个material文件,命名为M_Smoke。Shader选择Particles/Standard Surface,Rendering mode选择Fade。Main Option勾选Two Sided和Camera Fading。把刚才导入的材质贴图拖动到Maps的Albedo贴图中。这样就定义好材质了。

新建一个空的GameObject,命名为Impact Effect,保存为Prefab,然后进行编辑。

新增一个子物体,命名为Concrete Effect,添加一个Particle System的Component,具体设置如下:

模拟弹孔

在Asset的Texture目录导入一张弹孔的图片作为Texture,如下图:

在Asset的Material目录新建一个材质文件,命名为M_Impact_Concrete,shader选择Legacy Shaders/Particles/VertexLit Blended,然后把之前新建的Texture拖动到Particle Texture中。

在Impact Effect这个Prefab下新建一个子物体,命名为Bullet Hole,新建一个Particle System的Component,设置如下:

模拟水泥碎片

同样是找一个水泥碎片的图片导入到Asset的Texture目录中,如下图:

然后同样是新建一个材质,如之前弹孔的操作步骤。

在Impact Effect这个Prefab下新建一个子物体,命名为Concrete Debris,添加Particle system,配置如下:

 

代码实现

最后就是在Impact Effect这个Prefab上新增一个脚本代码,播放声音以及控制多长时间销毁,代码如下:

public class Impact : MonoBehaviour
{[Header("Impact Despawn Timer")]//How long before the impact is destroyedpublic float despawnTimer = 10.0f;[Header("Audio")]public AudioClip[] impactSounds;public AudioSource audioSource;// Start is called before the first frame updatevoid Start(){StartCoroutine (DespawnTimer ());//Get a random impact sound from the arrayaudioSource.clip = impactSounds[Random.Range(0, impactSounds.Length)];//Play the random impact soundaudioSource.Play();}// Update is called once per framevoid Update(){}private IEnumerator DespawnTimer() {//Wait for set amount of timeyield return new WaitForSeconds (despawnTimer);//Destroy the impact gameobjectDestroy (gameObject);}
}

编辑Bullet的脚本文件,增加以下代码:

...
[Header("Impact Effect Prefabs")]
public Transform []	concreteImpactPrefabs;private void OnCollisionEnter(Collision collision) {...if (collision.gameObject.CompareTag("Concrete")) {//Instantiate random impact prefab from arrayInstantiate (concreteImpactPrefabs [0], transform.position, Quaternion.LookRotation (collision.contacts [0].normal));//Destroy bullet objectDestroy(gameObject);}...
}

然后我们把场景中的水泥墙的预制件的Tag都设置为Concrete,这样当子弹发射碰撞到水泥墙时,就能看到相应的效果了。

实现爆炸效果

当子弹打中油桶之类的物体时,可以触发爆炸效果。

首先需要制作油桶爆炸后分成的碎块效果,可以通过Blender来实现。

把模型导入到Blender,然后安装Cell Fracture插件,选择物体->快速效果->Cell Fracture,调整Souce Limit的数值,这个表示物体破碎为多少块,这样插件就会自动进行随机的破碎,然后我们只保留破碎后的物体即可,导出为FBX格式。如下图所示:

由于油桶为空心物体,现在这样制作的爆炸碎片是实心的,为了进一步改进,我们可以复制多一个油桶物体,稍微缩小一点,然后放置在原来油桶里面,通过Blender的Object Bool Tool插件来进行布尔差值切割,这样就能制作一个空心的油桶了,然后再进行Cell Fracture,效果更好。

把导出的模型添加到Assets->Models目录,然后拖动到场景,再拖动到Assets->Prefabs目录,保存为一个Prefab。需要注意,这个Prefab下的所有碎片子物体,需要全部添加Rigidbody组件,并且不要勾选Kinematic。如果我们希望破碎后保留这些碎片,那么还需要增加Mesh Collider组件并勾选Vertex,不然的话破碎之后时看不到这些碎片的。

为了实现爆炸的粒子效果,我们可以下载Unity Asset Store里面的Particle Pack插件,里面提供了很多效果的粒子系统实现,非常好用。

对于原始的未爆炸的油桶Prefab,为其新增一个Explode的Tag,并且添加一个名为Debris的脚本文件,代码如下:

public class Debris : MonoBehaviour
{public GameObject Explosive_debris;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}public void OnDestroy(){GameObject o = Instantiate(Explosive_debris, transform.position, transform.rotation);o.GetComponent<Explode>().Explodsion();Destroy(gameObject);}
}

然后把我们刚才制作的爆炸后的油桶Prefab拖动到这个Script的Explosive_debris中。

在新的爆炸后的油桶Prefab上新增一个名为Explode的脚本文件,代码如下:

public class Explode : MonoBehaviour
{public GameObject explodeEffect;public float force = 500f;public float radius = 5f;public float destroyAfter = 2f;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}public void Explodsion() {StartCoroutine(PlayExplodeEffect ());var rbs = GetComponentsInChildren<Rigidbody>();foreach(var rb in rbs) {rb.AddExplosionForce(1000f, transform.position, 10f);}}private IEnumerator PlayExplodeEffect () {GameObject o = Instantiate(explodeEffect, transform.position, transform.rotation);yield return new WaitForSeconds (destroyAfter);Destroy(o);}
}

 然后把Particle Pack的BigExplosion拖动到这个Script的explosionEffect中。

最后就是修改一下Bullet的脚本文件,代码如下:

private void OnCollisionEnter(Collision collision) {...if (collision.gameObject.CompareTag("Explode")) {Debris dest = collision.gameObject.GetComponent<Debris>();if (dest != null) {dest.OnDestroy();}}Destroy(this.gameObject);
}

现在爆炸的效果就制作好了。 

游戏道具的拾取

为了增加游戏的趣味性,需要设计一些游戏道具,例如医药包,弹药等。

从网上找一个白色十字的3D模型作为医药包,导入到Assets->Models目录,然后保存为Prefab。

编辑这个Prefab,添加Rigidbody和Box Collider组件,其中Rigidbody需要勾选Is Kinematic,Box Collider需要勾选Is Trigger。

在Assets->Scripts目录新增一个Pickup的代码文件,代码如下:

[RequireComponent(typeof(Rigidbody), typeof(Collider))]
public class Pickup : MonoBehaviour
{[Tooltip("Frequency at which the item will move up and down")]public float VerticalBobFrequency = 1f;[Tooltip("Distance the item will move up and down")]public float BobbingAmount = 1f;[Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;[Tooltip("Sound played on pickup")] public AudioClip PickupSfx;[Tooltip("VFX spawned on pickup")] public GameObject PickupVfxPrefab;public Rigidbody PickupRigidbody { get; private set; }Collider m_Collider;Vector3 m_StartPosition;bool m_HasPlayedFeedback;protected virtual void Start(){PickupRigidbody = GetComponent<Rigidbody>();m_Collider = GetComponent<Collider>();// ensure the physics setup is a kinematic rigidbody triggerPickupRigidbody.isKinematic = true;m_Collider.isTrigger = true;// Remember start position for animationm_StartPosition = transform.position;}void Update(){// Handle bobbingfloat bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;// Handle rotatingtransform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);}void OnTriggerEnter(Collider other){PlayerController pickingPlayer = other.GetComponent<PlayerController>();if (pickingPlayer != null){OnPicked(pickingPlayer);}}protected virtual void OnPicked(PlayerController playerController){PlayPickupFeedback();}public void PlayPickupFeedback(){if (m_HasPlayedFeedback)return;if (PickupSfx){//AudioUtility.CreateSFX(PickupSfx, transform.position, AudioUtility.AudioGroups.Pickup, 0f);}if (PickupVfxPrefab){var pickupVfxInstance = Instantiate(PickupVfxPrefab, transform.position, Quaternion.identity);}m_HasPlayedFeedback = true;}
}

这个代码定义了所有道具的基本Pickup行为

对于医药包道具的Pickup,除了基本行为之外,还有个特殊的行为就是给玩家加血,因此定义一个HealthPickup的代码,如下:

public class HealthPickup : Pickup
{[Header("Parameters")] [Tooltip("Amount of health to heal on pickup")]public int HealAmount = 2;protected override void OnPicked(PlayerController player) {player.Heal(HealAmount);Destroy(gameObject);}
}

把这个代码添加到医药包Prefab上。

当玩家触碰到医药包时,就会回调PlayerController脚本的Heal方法,因此添加一个Heal方法,代码如下:

public void Heal(int point) {if (_health+point >= PlayerHealth) {_health = PlayerHealth;} else {_health += point;}float healthValue = (float) _health/PlayerHealth;ExecuteEvents.Execute<IGameMessage>(_gameManager, null, (x,y)=>x.HealthMessage(healthValue));
}

之后我们可以实现一个木箱子的破碎效果,当木箱子被打破后,就会露出医药包。具体定义木箱子破碎的过程和之前的类似,只是破碎效果不要选择爆炸,而是DustExplodsion。

完善UI界面

任务简介

最后就是完善整个游戏的UI界面,在游戏一开始增加任务介绍,当任务完成或失败时展示相应的界面。

首先是增加任务简介,在GameScreen这个Prefab下新建一个Text TMP,命名为Task,然后输入任务简介。编辑GameLevelLoader脚本文件,增加代码如下:

...
private long _startTick = 0;
private GameObject _task;void Start()
{..._startTick = DateTime.UtcNow.Ticks;_task = GameObject.Find("GameScreen/Task");
}void Update()
{long nowTS = DateTime.UtcNow.Ticks;float interval = (nowTS - _startTick)/10000000.0f;if (interval >= 10f) {if (_task) {_task.SetActive(false);}}
}

这个代码表示在游戏开始的头10秒显示这一关任务的简介。

任务完成或失败的场景显示

当这一关任务完成或失败时,应该显示对应的场景。

在Assets->Scenes的目录下,新建两个场景,分别命名为WinScene和LoseScene,每个场景都增加一个Canvas,然后在Canvas下新增一个背景图片,一个文字描述和一个按钮,如下图所示:

定义一个消息接口文件GameFlowMessage,用来传递设定场景任务以及消灭敌人等消息的传递,如以下代码:

public interface IGameFlowMessage : IEventSystemHandler
{void KillEnemyMessage();void FindTargetMessage();void SetObjectiveMessage(int killEnemyNumber, int findTargetNumber);void PlayerDeathMessage();    
}

 然后定义一个GameFlowController脚本文件,实现以上接口的方法,如以下代码:

public class GameFlowController : MonoBehaviour, IGameFlowMessage
{[Header("Win")] [Tooltip("This string has to be the name of the scene you want to load when winning")]public string WinSceneName = "WinScene";[Header("Lose")] [Tooltip("This string has to be the name of the scene you want to load when winning")]public string LoseSceneName = "LoseScene";private int _killEnemyNumber;private int _currentKillEnemy = 0;private int _findTargetNumber;private int _currentFindTarget = 0;public void KillEnemyMessage() {_currentKillEnemy++;Debug.LogFormat("Kill Enemy, current:{0}, target:{1}", _currentKillEnemy, _killEnemyNumber);if (_currentKillEnemy == _killEnemyNumber && _currentFindTarget == _findTargetNumber) {WinOrLose(true);}}public void FindTargetMessage() {_currentFindTarget++;if (_currentKillEnemy == _killEnemyNumber && _currentFindTarget == _findTargetNumber) {WinOrLose(true);}}public void SetObjectiveMessage(int killEnemyNumber, int findTargetNumber) {_killEnemyNumber = killEnemyNumber;_findTargetNumber = findTargetNumber;}public void PlayerDeathMessage() {WinOrLose(false);}public void WinOrLose(bool win) {Cursor.lockState = CursorLockMode.None;Cursor.visible = true;if (win) {SceneManager.LoadScene(WinSceneName);} else {SceneManager.LoadScene(LoseSceneName);}}
}

把这个Script加到GameLoader这个GameObject上面,然后编辑GameLevelLoader脚本,设定这一关任务,代码如下:

...
private GameFlowController _gameFlowController;void Start()
{..._gameFlowController = GetComponent<GameFlowController>();_gameFlowController.SetObjectiveMessage(9, 0);
}

修改NPC的WanderingAI代码,当NPC被消灭时,发送KillEnemyMessage,代码如下:

...
private GameObject _gameLoader;void Start()
{..._gameLoader = GameObject.Find("GameLoader");
}private IEnumerator Death() {...ExecuteEvents.Execute<IGameFlowMessage>(_gameLoader, null, (x,y)=>x.KillEnemyMessage());
}

总结

以上就是一个FPS游戏的制作过程,后续将继续完善玩家的换武器,以及关卡的设计。

相关文章:

Unity开发一个FPS游戏之三

在前面的两篇博客中&#xff0c;我已实现了一个FPS游戏的大部分功能&#xff0c;包括了第一人称的主角运动控制&#xff0c;武器射击以及敌人的智能行为。这里我将继续完善这个游戏&#xff0c;包括以下几个方面&#xff1a; 增加一个真实的游戏场景&#xff0c;模拟一个废弃的…...

NIUSHOP完美运营版商城 虚拟商品全功能商城 全能商城小程序 智慧商城系统 全品类百货商城

完美运营版商城/拼团/团购/秒杀/积分/砍价/实物商品/虚拟商品等全功能商城 干干净净 没有一丝多余收据 还没过手其他站 还没乱七八走的广告和后门 后台可以自由拖曳修改前端UI页面 还支持虚拟商品自动发货等功能 挺不错的一套源码 前端UNIAPP 后端PHP 一键部署版本 源码免费…...

vue2开发好还是vue3开发好vue3.0开发路线

Vue 2和Vue 3都是流行的前端框架&#xff0c;它们各自有一些特点和优势。选择Vue 2还是Vue 3进行开发&#xff0c;主要取决于你的项目需求、团队的技术栈、以及对新特性的需求等因素。以下是一些关于Vue 2和Vue 3的比较&#xff0c;帮助你做出决策&#xff1a; Vue 2&#xff1…...

爬虫 新闻网站 并存储到CSV文件 以红网为例 V2.0 (控制台版)升级自定义查询关键词、时间段,详细注释

爬虫&#xff1a;红网网站&#xff0c; 获取指定关键词与指定时间范围内的新闻&#xff0c;并存储到CSV文件 V2.0&#xff08;控制台版&#xff09; 爬取目的&#xff1a;为了获取某一地区更全面的在红网已发布的宣传新闻稿&#xff0c;同时也让自己的工作更便捷 对比V1.0升级的…...

JavaSE-11笔记【多线程2(+2024新)】

文章目录 6.线程安全6.1 线程安全问题6.2 线程同步机制6.3 关于线程同步的面试题6.3.1 版本16.3.2 版本26.3.3 版本36.3.4 版本4 7.死锁7.1 多线程卖票问题 8.线程通信8.1 wait()和sleep的区别&#xff1f;8.2 两个线程交替输出8.3 三个线程交替输出8.4 线程通信-生产者和消费者…...

WebKit是什么?

WebKit是一个开源的浏览器引擎&#xff0c;它用于呈现网页内容在许多现代浏览器中&#xff0c;包括Safari浏览器、iOS内置浏览器、以及一些其他浏览器如Google Chrome的早期版本。以下是一些关于WebKit的重要信息&#xff1a; 起源和发展&#xff1a;WebKit最初是由苹果公司为其…...

谷歌(Google)历年编程真题——接雨水

谷歌历年面试真题——数组和字符串系列真题练习。 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;…...

golang 归并回源策略

前言 下面是我根据业务需求画了一个架构图&#xff0c;没有特别之处&#xff0c;很普通&#xff0c;都是我们常见的中间件&#xff0c;都是一些幂等性GET 请求。有一个地方很有意思&#xff0c;从service 分别有10000 qps 请求到Redis&#xff0c;并且它们的key 是一样的。这样…...

【漏洞复现】可视化融合指挥调度平台 dispatch接口处存在任意文件上传漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…...

最讨厌这种字符串问题了!!

题目&#xff1a;洛谷P1957口算练习题 题目大意描述&#xff1a; 第一行输入一个整数表示接下来要进行多少次运算&#xff0c;接下来每行输入一个字母c和两个数字x,y&#xff08;输入的字母为a/b/c,分别表示要进行&#xff0c;-&#xff0c;*运算&#xff09;或者就输入两个数…...

B-名牌赌王(本人遇到的题,做个笔记)

题解&#xff1a; #include <iostream> #include <queue> //需要用小根堆的优先队列 #include <unordered_map> //用无序映射 using namespace std; bool pai() {int n, m;cin >> n >> m; priority_queue<int, vector<int>, gr…...

博客评论回复03

接着之前写的&#xff0c;之前返回的数据集按道理来说渲染出来还是丑丑的&#xff0c;因此这次我看着抖音的评论样子&#xff0c;自己瞎写了一通&#xff0c;不过也算是模仿出来了虽然肯定没有抖音写的好。 类似与前面几章写的表结构 首先看看抖音评论区是怎么样的&#xff1f…...

【【萌新的学习之Numpy数组的使用】】

萌新的学习之Numpy数组的使用 先记录一下之前的关于函数的设计 通过创造类的形式 复习完毕之后介绍numpy数组的使用 #整数型数组遇到除法 &#xff08;即便是除以整数&#xff09; 不同维度的数组之间 从外形上的本质区别 一维数组用1层中括号 二维数组用2层中括号 三维数…...

RabbitMQ3.13.x之七_RabbitMQ消息队列模型

RabbitMQ3.13.x之七_RabbitMQ消息队列模型 文章目录 RabbitMQ3.13.x之七_RabbitMQ消息队列模型1. RabbitMQ消息队列模型1. 简单队列2. Work Queues(工作队列)3. Publish/Subscribe(发布/订阅)4. Routing(路由)5. Topics(主题)6. RPC(远程过程调用)7. Publisher Confirms(发布者…...

Android JNI 调用第三方SO

最近一个项目使用了Go 编译了一个so库&#xff0c;但是这个so里面还需要使用第三方so库pdfium, 首先在Android工程把2个so库都放好 在jni中只能使用dlopen方式&#xff0c;其他的使用函数指针的方式来调用&#xff0c;和windows dll类似&#xff0c;不然虽然编译过了但是会崩溃…...

Vid2seq

Vid2Seq 应该是目前为止,个人最中意得一篇能够实际解决对一段视频进行粗略理解得paper了。个人认为它能够真正能解决视频理解是因为它是对一个模型整体做了训练,而不仅仅是通过visual encoders(e.g BLIP/CLIP/…)和 其它multi modal 的encoder直接过了个projection,做一个…...

Opencv人机交互界面设置

Opencv人机交互界面设置 以下是一些常见的OpenCV人机交互界面设置&#xff1a; 窗口交互 显示窗口&#xff1a;可以使用cv2.imshow()函数在屏幕上显示图像。例如&#xff0c;要显示名为“image”的图像&#xff0c;可以使用以下代码&#xff1a; import cv2img cv2.imread…...

蓝桥杯算法心得——字典树考试(贡献度+前缀和)

大家好&#xff0c;我是晴天学长&#xff0c;贡献度的题&#xff0c;找到技巧非常重要&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .字典树考试 字典树考试 问题描述 蓝桥学院最近教学了字典树这一数…...

Linux下Qt生成程序崩溃文件

文章目录 1.背景2.Qt编译生成程序2.1.profile模式的本质 3.执行程序&#xff0c;得到core文件4.代码定位4.1.直接使用gdb4.2.使用QtCreator 5.总结6.题外话6.1.profile模式和debug模式的区别 1.背景 在使用Qt时&#xff0c;假如在windows&#xff0c;当软件崩溃时&#xff0c;…...

Go语言中测试和性能

1. 测试:软件开发最重要的方面 测试软件程序可能是软件开发人员能够做的最重要的事情。通过测试代码的功能,开发人员能够在很大程度上确定程序是有效的。另外,每次修改代码后,开发人员都可运行测试,确认没有引入Bug和衰退。通过测试软件,还能够让软件工程师确认程序按期望…...

回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测

回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测 目录 回归预测 | Matlab基于CPO-GPR基于冠豪猪算法优化高斯过程回归的多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab基于CPO-GPR基于冠豪猪算法优化高斯…...

python 日期字符串转换为指定格式的日期

在Python编程中&#xff0c;日期处理是一个常见的任务。我们经常需要将日期字符串转换为Python的日期对象&#xff0c;以便进行日期的计算、比较或其他操作。同时&#xff0c;为了满足不同的需求&#xff0c;我们还需要将日期对象转换为指定格式的日期字符串。本文将详细介绍如…...

day03-Docker

1.初识 Docker 1.1.什么是 Docker 1.1.1.应用部署的环境问题 大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题&#xff1a; 依赖关系复杂&#xff0c;容易出现兼容性问题开发、测试、生产环境有差异 例如一个项目中&#xff0c;部署时需要依…...

C语言函数实现冒泡排序

前言 今天我们来看看怎么使用函数的方式实现冒泡排序吧&#xff0c;我们以一个数组为例arr[] {9,8,7,6,5,4,3,2,1,0},我们将这个数组通过冒泡排序的方式让他变为升序吧。 代码实现 #include<stdio.h> void bubble_sort(int arr[], int sz) {int i 0;for (i 0;i < s…...

区间概率预测python|QR-CNN-BiLSTM+KDE分位数-卷积-双向长短期记忆神经网络-时间序列区间概率预测+核密度估计

区间预测python|QR-CNN-BiLSTMKDE分位数-卷积-双向长短期记忆神经网络-核密度估计-回归时间序列区间预测 模型输出展示&#xff1a; (图中是只设置了20次迭代的预测结果&#xff0c;宽度较宽&#xff0c;可自行修改迭代参数&#xff0c;获取更窄的预测区间&#xff09; 注&am…...

Java 分支结构 - if…else/switch

顺序结构只能顺序执行&#xff0c;不能进行判断和选择&#xff0c;因此需要分支结构。 Java有两种分支结构&#xff1a; if语句switch语句 if语句 一个if语句包含一个布尔表达式和一条或多条语句。 语法 If 语句的用语法如下&#xff1a; if(布尔表达式) {//如果布尔表达…...

【Unity每日一记】如何从0到1将特效图集制作成一个特效

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…...

磁力链接的示例与解释

磁力链接&#xff08;Magnet URI scheme&#xff09;是一种特殊类型的统一资源标识符&#xff08;URI&#xff09;&#xff0c;它包含了通过特定散列函数&#xff08;如SHA-1&#xff09;得到的文件内容的散列值&#xff0c;而不是基于位置或名称的引用。这使得磁力链接成为在分…...

云存储中常用的相同子策略的高效、安全的基于属性的访问控制的论文阅读

参考文献为2022年发表的Efficient and Secure Attribute-Based Access Control With Identical Sub-Policies Frequently Used in Cloud Storage 动机 ABE是实现在云存储中一种很好的访问控制手段,但是其本身的计算开销导致在实际场景中应用收到限制。本论文研究了一种LSSS矩…...

JVM高级篇之GC

文章目录 版权声明垃圾回收器的技术演进ShenandoahShenandoah GC体验Shenandoah GC循环过程 ZGCZGC简介ZGC的版本更迭ZGC体验&使用ZGC的参数设置ZGC的调优 版权声明 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明&#xff0c;所有版权属于黑马…...

长宁网站设计/西安seo经理

随着移动终端的屏幕愈来愈大&#xff0c;显示效果和分辨率稳步提升&#xff0c;因此&#xff0c;在移动终端上进行报表展示逐渐可行。 本次来讨论一下移动终端上的报表展示&#xff0c;以Android为例&#xff0c;也不仅仅局限于Android&#xff0c;稍后会有Windows Phone 7.1上…...

做网站用属于前端/新闻 最新消息

在Python中&#xff0c;可以使用OpenCV。这里是instructions to install OpenCV in Python&#xff0c;如果您的系统中没有它。我想你也可以用其他的库来做同样的事情&#xff0c;过程也是一样的&#xff0c;诀窍是把蒙版反转并应用到某个背景上&#xff0c;你将得到你的蒙版图…...

上海网站建设代/站长工具视频

打开文件 打开文件夹&#xff0c;供操作者选择文件: OpenFileDialog dialog new OpenFileDialog();dialog.ShowDialog(); 获得所选文件的文件名: string fileNamedialoa.FileName; 获取所选文件的文件夹目录: FileInfo fi new FileInfo(t); string path fileName.Substring(…...

小型企业网站建设公司/疫情最新官方消息

在D3D11应用程序中&#xff0c;我们按下altenter键&#xff0c;会切换到全屏模式。有时候&#xff0c;我们在WM_SIZE中有一些代码&#xff0c;全屏时&#xff0c;会使得程序崩溃&#xff0c;比如之前教程的代码&#xff0c;就是如此。 下面我们在D3DClass.cpp 中&#xff0c;增…...

成都网站建设公司浅谈/免费职业技能培训网

单线程并发import time def consumer(name):print("%s 准备吃包子了"%name)while True:baoziyield#变成一个生成器&#xff0c;遇到yield断点。print("包子[%s]来了,被[%s]吃了" %(baozi,name)) cconsumer("hunter") # c.__next__() # c.send(&…...

京东网上商城跟京东是一家吗/站长工具查询seo

2019独角兽企业重金招聘Python工程师标准>>> Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一&#xff0c;大家可以查看RFC2045&#xff5e;RFC2049&#xff0c;上面有MIME的详细规范。 Base64要求把每三个8Bit的字节转换为四个6Bit的字节&#xff08;3*…...