... | ... | @@ -2,6 +2,7 @@ |
|
|
title: Night of Horror
|
|
|
---
|
|
|
|
|
|
|
|
|
[Toc]
|
|
|
# Game Design Document
|
|
|
|
... | ... | @@ -106,11 +107,434 @@ title: Night of Horror |
|
|
* Source: Get monster models from Unity Asset Store.
|
|
|
|
|
|
- **Technical Details**
|
|
|
* Random Generation
|
|
|
+ Use empty objects in the scene as generation points, and implement random generation logic of crystals and monsters through scripts.
|
|
|
* **Monster AI**
|
|
|
+ Monster behavior is implemented based on the Unity NavMesh system, covering patrol path planning and dynamic target tracking.
|
|
|
+ Monsters use state machines to achieve states in different situations, such as standby state (patrol), normal pursuit state (the player has not picked up the crystal yet), and global pursuit state (the player has picked up the crystal).
|
|
|
* **Player input acquisition and movement logic**
|
|
|
+ Use Unity's Input System to unify management, combine with Cinemachine components to achieve perspective control, and use Rigidbody components to control the movement of characters.
|
|
|
+ Main functions include:
|
|
|
* Input management
|
|
|
+ Use Input System to create a custom input action set to handle player movement and UI operations respectively.
|
|
|
* Character state management
|
|
|
+ A state system designed based on a finite state machine, in which PlayerMovingState is responsible for handling movement logic.
|
|
|
* Movement implementation
|
|
|
+ Use the camera direction to dynamically calculate the movement direction, and update the character's position and orientation through the rigid body to ensure that the physical performance of the player's movement is realistic and smooth.
|
|
|
+ Keycode
|
|
|
```csharp
|
|
|
using System.Collections;
|
|
|
using System.Collections.Generic;
|
|
|
using UnityEngine;
|
|
|
|
|
|
public class PlayerInput : MonoBehaviour
|
|
|
{
|
|
|
public PlayerInputActions inputActions { get; private set; }
|
|
|
|
|
|
public PlayerInputActions.PlayerActions playerActions { get; private set; }
|
|
|
public PlayerInputActions.UIActions uiActions { get; private set; }
|
|
|
|
|
|
private void Awake()
|
|
|
{
|
|
|
inputActions = new PlayerInputActions();
|
|
|
|
|
|
playerActions = inputActions.Player;
|
|
|
uiActions = inputActions.UI;
|
|
|
}
|
|
|
|
|
|
private void OnEnable()
|
|
|
{
|
|
|
inputActions.Enable();
|
|
|
}
|
|
|
|
|
|
private void OnDisable()
|
|
|
{
|
|
|
inputActions.Disable();
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
```csharp
|
|
|
using System;
|
|
|
using System.Collections;
|
|
|
using System.Collections.Generic;
|
|
|
using UnityEngine;
|
|
|
|
|
|
public class PlayerMovingState : PlayerState
|
|
|
{
|
|
|
protected Vector2 movementInput;
|
|
|
|
|
|
protected float movementSpeed = 3f;
|
|
|
|
|
|
public PlayerMovingState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
|
|
|
{
|
|
|
}
|
|
|
|
|
|
public override void Enter()
|
|
|
{
|
|
|
base.Enter();
|
|
|
|
|
|
ResetCameraRotation();
|
|
|
}
|
|
|
|
|
|
|
|
|
public override void Update()
|
|
|
{
|
|
|
base.Update();
|
|
|
|
|
|
MovementInput();
|
|
|
}
|
|
|
|
|
|
public override void PhysicsUpdate()
|
|
|
{
|
|
|
base.PhysicsUpdate();
|
|
|
|
|
|
Move();
|
|
|
}
|
|
|
|
|
|
public override void Exit()
|
|
|
{
|
|
|
base.Exit();
|
|
|
}
|
|
|
|
|
|
private void ResetCameraRotation()
|
|
|
{
|
|
|
Vector3 cameraForward = stateMachine.Player.cameraTransform.forward;
|
|
|
cameraForward.y = 0;
|
|
|
cameraForward.Normalize();
|
|
|
|
|
|
Quaternion rotation = Quaternion.LookRotation(cameraForward);
|
|
|
|
|
|
stateMachine.Player.playerRigidbody.rotation = rotation;
|
|
|
}
|
|
|
|
|
|
private void MovementInput()
|
|
|
{
|
|
|
movementInput = stateMachine.Player.input.playerActions.Movement.ReadValue<Vector2>();
|
|
|
}
|
|
|
|
|
|
private void Move()
|
|
|
{
|
|
|
Vector3 cameraForward = stateMachine.Player.cameraTransform.forward;
|
|
|
cameraForward.y = 0;
|
|
|
cameraForward.Normalize();
|
|
|
|
|
|
Vector3 cameraRight = stateMachine.Player.cameraTransform.right;
|
|
|
cameraRight.y = 0;
|
|
|
cameraRight.Normalize();
|
|
|
|
|
|
Vector3 moveDirection = cameraForward * movementInput.y + cameraRight * movementInput.x;
|
|
|
|
|
|
if (moveDirection != Vector3.zero)
|
|
|
{
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
|
|
|
float rotationSpeed = 0.5f;
|
|
|
stateMachine.Player.playerRigidbody.rotation = Quaternion.Slerp(stateMachine.Player.playerRigidbody.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
|
|
|
|
|
stateMachine.Player.playerRigidbody.velocity = moveDirection.normalized * movementSpeed;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
stateMachine.Player.playerRigidbody.velocity = Vector3.zero;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
* Monster spawning and patrol logic
|
|
|
+ This module implements the monster spawning and patrol logic through the following steps:
|
|
|
* Spawning position and patrol target initialization
|
|
|
+ Place multiple empty objects in the scene as monster spawning points and patrol target points respectively. Load these points into the list through code for subsequent logic use.
|
|
|
* Monster spawning logic
|
|
|
+ According to the preset number of monsters, randomly select spawning points from the spawning point list and instantiate monsters at the corresponding coordinate positions. Each generated monster comes with a patrol point list for subsequent patrol logic use.
|
|
|
* Patrol logic
|
|
|
+ The monster's initial state is patrol mode (standby state), and navigation is implemented using NavMeshAgent. The monster randomly selects target points from the patrol point list and navigates to the target points one by one.
|
|
|
* State switching logic
|
|
|
+ During the patrol, the monster will constantly detect whether the player has entered its detection range (determined by distance and angle). If the player enters the range, the current monster will switch to the pursuit state. Under certain conditions (the player obtains the crystal), all monsters switch to the pursuit state.
|
|
|
+ Keycode
|
|
|
* Monster generation and patrol target initialization
|
|
|
```csharp
|
|
|
private void InitialiseSpawnPoints()
|
|
|
{
|
|
|
monsterSpawnPoints.Clear();
|
|
|
foreach (Transform spawnPoint in monsterSpawnPointsSet.transform)
|
|
|
{
|
|
|
monsterSpawnPoints.Add(spawnPoint);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void InitializeMonster()
|
|
|
{
|
|
|
for (int i = 0; i < monsterCount; i++)
|
|
|
{
|
|
|
GameObject newMonster = Instantiate(monster);
|
|
|
newMonster.transform.parent = this.transform;
|
|
|
|
|
|
Monster activeMonster = newMonster.GetComponent<Monster>();
|
|
|
if (activeMonster != null)
|
|
|
{
|
|
|
monsters.Add(activeMonster);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public Vector3 GetRandomSpawnLocation()
|
|
|
{
|
|
|
int spawnIndex = Random.Range(0, monsterSpawnPoints.Count);
|
|
|
Transform spawnTransform = monsterSpawnPoints[spawnIndex];
|
|
|
monsterSpawnPoints.RemoveAt(spawnIndex);
|
|
|
return spawnTransform.position;
|
|
|
}
|
|
|
```
|
|
|
* Patrol logic implementation
|
|
|
```csharp
|
|
|
private void Patrol()
|
|
|
{
|
|
|
stateMachine.Monster.navMeshAgent.SetDestination(patrolTarget);
|
|
|
}
|
|
|
|
|
|
private bool ReachPatrolTarget()
|
|
|
{
|
|
|
float distanceToTarget = Vector3.Distance(stateMachine.Monster.transform.position, patrolTarget);
|
|
|
return distanceToTarget <= stateMachine.Monster.navMeshAgent.stoppingDistance;
|
|
|
}
|
|
|
|
|
|
private void SetNewPatrolTarget()
|
|
|
{
|
|
|
if (stateMachine.Monster.patrolPoints.Count > 0)
|
|
|
{
|
|
|
int randomIndex = Random.Range(0, stateMachine.Monster.patrolPoints.Count);
|
|
|
patrolTarget = stateMachine.Monster.patrolPoints[randomIndex].position;
|
|
|
|
|
|
NavMeshHit hit;
|
|
|
if (NavMesh.SamplePosition(patrolTarget, out hit, 1.0f, NavMesh.AllAreas))
|
|
|
{
|
|
|
patrolTarget = hit.position;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
* State switching
|
|
|
```csharp
|
|
|
private bool DetectPlayer()
|
|
|
{
|
|
|
if (player == null) return false;
|
|
|
|
|
|
Vector3 directionToPlayer = (player.transform.position - stateMachine.Monster.transform.position).normalized;
|
|
|
float distanceToPlayer = Vector3.Distance(stateMachine.Monster.transform.position, player.transform.position);
|
|
|
|
|
|
if (distanceToPlayer <= stateMachine.Monster.dectectionRange)
|
|
|
{
|
|
|
float angleToPlayer = Vector3.Angle(stateMachine.Monster.transform.forward, directionToPlayer);
|
|
|
if (angleToPlayer <= stateMachine.Monster.dectectionAngle / 2)
|
|
|
{
|
|
|
if (!Physics.Raycast(stateMachine.Monster.transform.position, directionToPlayer, distanceToPlayer, LayerMask.GetMask("Monster")))
|
|
|
{
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
```
|
|
|
```csharp
|
|
|
public void ActivateGlobalChase()
|
|
|
{
|
|
|
foreach (Monster activeMonster in monsters)
|
|
|
{
|
|
|
if (activeMonster != null)
|
|
|
{
|
|
|
activeMonster.ActivateGlobalChaseState();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
* Monster pursuit logic
|
|
|
+ Normal pursuit mode logic
|
|
|
* Detects whether the player is within the monster's pursuit range, including distance and angle detection.
|
|
|
* If the player is within the range, the monster navigates to the player's position through NavMeshAgent.SetDestination.
|
|
|
* If the player is out of range, the timer starts, and if the player is not re-detected after the set time, it returns to the patrol state.
|
|
|
* If the distance to the player is close enough (capture distance), the monster enters the player capture logic and stops all monster actions.
|
|
|
+ Global pursuit mode logic
|
|
|
* The monster directly navigates to the player's real-time position, ignoring the range limit.
|
|
|
* When the monster approaches the player and reaches the capture distance, the capture logic is triggered.
|
|
|
* The game ends after the player is captured.
|
|
|
* Keycode
|
|
|
+ Normal pursuit
|
|
|
```csharp
|
|
|
private void Pursuit()
|
|
|
{
|
|
|
if (player == null)
|
|
|
{
|
|
|
stateMachine.ChangeState(stateMachine.monsterIdlingState);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
stateMachine.Monster.navMeshAgent.SetDestination(player.transform.position);
|
|
|
|
|
|
if (PlayerInChaseRange())
|
|
|
{
|
|
|
lostPlayerTimel = 0f;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
lostPlayerTimel += Time.deltaTime;
|
|
|
|
|
|
if (lostPlayerTimel >= maxlostEnemyTimel)
|
|
|
{
|
|
|
stateMachine.ChangeState(stateMachine.monsterIdlingState);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
float distanceToPlayer = Vector3.Distance(stateMachine.Monster.transform.position, player.transform.position);
|
|
|
if (distanceToPlayer <= catchDistance && !hasCaughtPlayer)
|
|
|
{
|
|
|
CatchPlayer();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void CatchPlayer()
|
|
|
{
|
|
|
hasCaughtPlayer = true;
|
|
|
|
|
|
stateMachine.Monster.navMeshAgent.isStopped = true;
|
|
|
stateMachine.Monster.navMeshAgent.ResetPath();
|
|
|
|
|
|
stateMachine.Monster.animator.enabled = false;
|
|
|
|
|
|
MonsterManage monsterManage = GameObject.Find("MonsterManage").GetComponent<MonsterManage>();
|
|
|
if (monsterManage != null)
|
|
|
{
|
|
|
monsterManage.StopAllMonster(true);
|
|
|
}
|
|
|
|
|
|
if (player != null)
|
|
|
{
|
|
|
player.GetComponent<Player>().GameOver();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private bool PlayerInChaseRange()
|
|
|
{
|
|
|
Vector3 directionToPlayer = (player.transform.position - stateMachine.Monster.transform.position).normalized;
|
|
|
float distanceToPlayer = Vector3.Distance(stateMachine.Monster.transform.position, player.transform.position);
|
|
|
|
|
|
if (distanceToPlayer <= stateMachine.Monster.dectectionRange)
|
|
|
{
|
|
|
float angleToPlayer = Vector3.Angle(stateMachine.Monster.transform.forward, directionToPlayer);
|
|
|
|
|
|
if (angleToPlayer <= stateMachine.Monster.dectectionAngle / 2)
|
|
|
{
|
|
|
if (!Physics.Raycast(stateMachine.Monster.transform.position, directionToPlayer, distanceToPlayer, LayerMask.GetMask("Environment")))
|
|
|
{
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
```
|
|
|
+ Global pursuit
|
|
|
```csharp
|
|
|
private void Pursuit()
|
|
|
{
|
|
|
if (player == null)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
stateMachine.Monster.navMeshAgent.SetDestination(player.transform.position);
|
|
|
|
|
|
float distanceToPlayer = Vector3.Distance(stateMachine.Monster.transform.position, player.transform.position);
|
|
|
if (distanceToPlayer <= catchDistance && !hasCaughtPlayer)
|
|
|
{
|
|
|
CatchPlayer();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void CatchPlayer()
|
|
|
{
|
|
|
hasCaughtPlayer = true;
|
|
|
|
|
|
stateMachine.Monster.navMeshAgent.isStopped = true;
|
|
|
stateMachine.Monster.navMeshAgent.ResetPath();
|
|
|
|
|
|
stateMachine.Monster.animator.enabled = false;
|
|
|
|
|
|
MonsterManage monsterManage = GameObject.Find("MonsterManage")?.GetComponent<MonsterManage>();
|
|
|
if (monsterManage != null)
|
|
|
{
|
|
|
monsterManage.StopAllMonster(true);
|
|
|
}
|
|
|
|
|
|
if (player != null)
|
|
|
{
|
|
|
player.GameOver();
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
* Crystal generation and picking up
|
|
|
+ Crystal generation:
|
|
|
* Generates a crystal by randomly selecting a point from the preset generation points.
|
|
|
+ Crystal picking:
|
|
|
* When the player touches the crystal, the interactive UI is displayed. After the player presses the interaction key, the crystal is destroyed and subsequent events are triggered.
|
|
|
+ Trigger event:
|
|
|
* After the crystal is picked up, the logic of the monster's global pursuit will be triggered.
|
|
|
*Keycode
|
|
|
```csharp
|
|
|
private void InitialiseSpawnPoint()
|
|
|
{
|
|
|
crystalSpawnPoints.Clear();
|
|
|
|
|
|
foreach (Transform spawnPoint in crystalSpawnPointSet.transform)
|
|
|
{
|
|
|
crystalSpawnPoints.Add(spawnPoint);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void InitialCrystal()
|
|
|
{
|
|
|
int spawnIndex = Random.Range(0, crystalSpawnPoints.Count);
|
|
|
Transform spawnTransform = crystalSpawnPoints[spawnIndex];
|
|
|
|
|
|
this.transform.position = spawnTransform.position;
|
|
|
}
|
|
|
|
|
|
private void OnTriggerEnter(Collider other)
|
|
|
{
|
|
|
if (other.CompareTag("Player"))
|
|
|
{
|
|
|
interactionUI.SetActive(true);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void OnTriggerStay(Collider other)
|
|
|
{
|
|
|
if (player.input.uiActions.Interact.IsPressed())
|
|
|
{
|
|
|
Destroy(gameObject);
|
|
|
interactionUI.SetActive(false);
|
|
|
|
|
|
ElevatorManagement elevator = GameObject.Find("Hotel").GetComponent<ElevatorManagement>();
|
|
|
if (elevator != null)
|
|
|
{
|
|
|
elevator.OnCrystalCollected();
|
|
|
}
|
|
|
|
|
|
MonsterManage monsterManage = GameObject.Find("MonsterManage").GetComponent<MonsterManage>();
|
|
|
if (monsterManage != null)
|
|
|
{
|
|
|
monsterManage.ActivateGlobalChase();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void OnTriggerExit(Collider other)
|
|
|
{
|
|
|
if (other.CompareTag("Player"))
|
|
|
{
|
|
|
interactionUI.SetActive(false);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
|
|
|
## Game design reflections and improvement suggestions
|
|
|
|
... | ... | |