So erstellen Sie einen FPS mit der KI-Unterstützung in Unity
Ego-Shooter (FPS) ist ein Subgenre von Shooter-Spielen, bei dem der Spieler aus der Ego-Perspektive gesteuert wird.
Um ein FPS-Spiel in Unity zu erstellen, benötigen wir einen Spieler-Controller, eine Reihe von Gegenständen (in diesem Fall Waffen) und die Feinde.
Schritt 1: Erstellen Sie den Player-Controller
Hier erstellen wir einen Controller, der von unserem Spieler verwendet wird.
- Erstellen Sie ein neues Spielobjekt (Spielobjekt -> Leer erstellen) und benennen Sie es "Player"
- Erstellen Sie eine neue Kapsel (Spielobjekt -> 3D-Objekt -> Kapsel) und verschieben Sie sie in das "Player"-Objekt
- Entfernen Sie die Capsule Collider-Komponente aus der Capsule und ändern Sie ihre Position in (0, 1, 0).
- Bewegen Sie die Hauptkamera in das Objekt "Player" und ändern Sie ihre Position in (0, 1,64, 0).
- Erstellen ein neues Skript, nennen Sie es "SC_CharacterController" und fügen Sie den folgenden Code ein:
SC_CharacterController.cs
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class SC_CharacterController : MonoBehaviour
{
public float speed = 7.5f;
public float jumpSpeed = 8.0f;
public float gravity = 20.0f;
public Camera playerCamera;
public float lookSpeed = 2.0f;
public float lookXLimit = 45.0f;
CharacterController characterController;
Vector3 moveDirection = Vector3.zero;
Vector2 rotation = Vector2.zero;
[HideInInspector]
public bool canMove = true;
void Start()
{
characterController = GetComponent<CharacterController>();
rotation.y = transform.eulerAngles.y;
}
void Update()
{
if (characterController.isGrounded)
{
// We are grounded, so recalculate move direction based on axes
Vector3 forward = transform.TransformDirection(Vector3.forward);
Vector3 right = transform.TransformDirection(Vector3.right);
float curSpeedX = canMove ? speed * Input.GetAxis("Vertical") : 0;
float curSpeedY = canMove ? speed * Input.GetAxis("Horizontal") : 0;
moveDirection = (forward * curSpeedX) + (right * curSpeedY);
if (Input.GetButton("Jump") && canMove)
{
moveDirection.y = jumpSpeed;
}
}
// Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below
// when the moveDirection is multiplied by deltaTime). This is because gravity should be applied
// as an acceleration (ms^-2)
moveDirection.y -= gravity * Time.deltaTime;
// Move the controller
characterController.Move(moveDirection * Time.deltaTime);
// Player and Camera rotation
if (canMove)
{
rotation.y += Input.GetAxis("Mouse X") * lookSpeed;
rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed;
rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit);
playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0);
transform.eulerAngles = new Vector2(0, rotation.y);
}
}
}
- Hängen Sie das SC_CharacterController-Skript an das Objekt "Player" an (Sie werden feststellen, dass es auch eine weitere Komponente namens Character Controller hinzugefügt hat und deren Mittelwert in (0, 1, 0) geändert wurde.)
- Weisen Sie die Hauptkamera der Player-Kamera-Variable in SC_CharacterController zu
Der Player-Controller ist jetzt bereit:
Schritt 2: Erstellen Sie das Waffensystem
Das Waffensystem des Spielers besteht aus drei Komponenten: einem Waffenmanager, einem Waffenskript und einem Bullet-Skript.
- Erstellen ein neues Skript, nennen Sie es "SC_WeaponManager" und fügen Sie den folgenden Code ein:
SC_WeaponManager.cs
using UnityEngine;
public class SC_WeaponManager : MonoBehaviour
{
public Camera playerCamera;
public SC_Weapon primaryWeapon;
public SC_Weapon secondaryWeapon;
[HideInInspector]
public SC_Weapon selectedWeapon;
// Start is called before the first frame update
void Start()
{
//At the start we enable the primary weapon and disable the secondary
primaryWeapon.ActivateWeapon(true);
secondaryWeapon.ActivateWeapon(false);
selectedWeapon = primaryWeapon;
primaryWeapon.manager = this;
secondaryWeapon.manager = this;
}
// Update is called once per frame
void Update()
{
//Select secondary weapon when pressing 1
if (Input.GetKeyDown(KeyCode.Alpha1))
{
primaryWeapon.ActivateWeapon(false);
secondaryWeapon.ActivateWeapon(true);
selectedWeapon = secondaryWeapon;
}
//Select primary weapon when pressing 2
if (Input.GetKeyDown(KeyCode.Alpha2))
{
primaryWeapon.ActivateWeapon(true);
secondaryWeapon.ActivateWeapon(false);
selectedWeapon = primaryWeapon;
}
}
}
- Erstellen Sie ein neues Skript, nennen Sie es "SC_Weapon" und fügen Sie den folgenden Code ein:
SC_Weapon.cs
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(AudioSource))]
public class SC_Weapon : MonoBehaviour
{
public bool singleFire = false;
public float fireRate = 0.1f;
public GameObject bulletPrefab;
public Transform firePoint;
public int bulletsPerMagazine = 30;
public float timeToReload = 1.5f;
public float weaponDamage = 15; //How much damage should this weapon deal
public AudioClip fireAudio;
public AudioClip reloadAudio;
[HideInInspector]
public SC_WeaponManager manager;
float nextFireTime = 0;
bool canFire = true;
int bulletsPerMagazineDefault = 0;
AudioSource audioSource;
// Start is called before the first frame update
void Start()
{
bulletsPerMagazineDefault = bulletsPerMagazine;
audioSource = GetComponent<AudioSource>();
audioSource.playOnAwake = false;
//Make sound 3D
audioSource.spatialBlend = 1f;
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0) && singleFire)
{
Fire();
}
if (Input.GetMouseButton(0) && !singleFire)
{
Fire();
}
if (Input.GetKeyDown(KeyCode.R) && canFire)
{
StartCoroutine(Reload());
}
}
void Fire()
{
if (canFire)
{
if (Time.time > nextFireTime)
{
nextFireTime = Time.time + fireRate;
if (bulletsPerMagazine > 0)
{
//Point fire point at the current center of Camera
Vector3 firePointPointerPosition = manager.playerCamera.transform.position + manager.playerCamera.transform.forward * 100;
RaycastHit hit;
if (Physics.Raycast(manager.playerCamera.transform.position, manager.playerCamera.transform.forward, out hit, 100))
{
firePointPointerPosition = hit.point;
}
firePoint.LookAt(firePointPointerPosition);
//Fire
GameObject bulletObject = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
SC_Bullet bullet = bulletObject.GetComponent<SC_Bullet>();
//Set bullet damage according to weapon damage value
bullet.SetDamage(weaponDamage);
bulletsPerMagazine--;
audioSource.clip = fireAudio;
audioSource.Play();
}
else
{
StartCoroutine(Reload());
}
}
}
}
IEnumerator Reload()
{
canFire = false;
audioSource.clip = reloadAudio;
audioSource.Play();
yield return new WaitForSeconds(timeToReload);
bulletsPerMagazine = bulletsPerMagazineDefault;
canFire = true;
}
//Called from SC_WeaponManager
public void ActivateWeapon(bool activate)
{
StopAllCoroutines();
canFire = true;
gameObject.SetActive(activate);
}
}
- Erstellen Sie ein neues Skript, nennen Sie es "SC_Bullet" und fügen Sie den folgenden Code ein:
SC_Bullet.cs
using System.Collections;
using UnityEngine;
public class SC_Bullet : MonoBehaviour
{
public float bulletSpeed = 345;
public float hitForce = 50f;
public float destroyAfter = 3.5f;
float currentTime = 0;
Vector3 newPos;
Vector3 oldPos;
bool hasHit = false;
float damagePoints;
// Start is called before the first frame update
IEnumerator Start()
{
newPos = transform.position;
oldPos = newPos;
while (currentTime < destroyAfter && !hasHit)
{
Vector3 velocity = transform.forward * bulletSpeed;
newPos += velocity * Time.deltaTime;
Vector3 direction = newPos - oldPos;
float distance = direction.magnitude;
RaycastHit hit;
// Check if we hit anything on the way
if (Physics.Raycast(oldPos, direction, out hit, distance))
{
if (hit.rigidbody != null)
{
hit.rigidbody.AddForce(direction * hitForce);
IEntity npc = hit.transform.GetComponent<IEntity>();
if (npc != null)
{
//Apply damage to NPC
npc.ApplyDamage(damagePoints);
}
}
newPos = hit.point; //Adjust new position
StartCoroutine(DestroyBullet());
}
currentTime += Time.deltaTime;
yield return new WaitForFixedUpdate();
transform.position = newPos;
oldPos = newPos;
}
if (!hasHit)
{
StartCoroutine(DestroyBullet());
}
}
IEnumerator DestroyBullet()
{
hasHit = true;
yield return new WaitForSeconds(0.5f);
Destroy(gameObject);
}
//Set how much damage this bullet will deal
public void SetDamage(float points)
{
damagePoints = points;
}
}
Jetzt werden Sie feststellen, dass das SC_Bullet-Skript einige Fehler aufweist. Das liegt daran, dass wir noch eine letzte Sache zu tun haben, nämlich die IEntity-Schnittstelle zu definieren.
Schnittstellen in C# sind nützlich, wenn Sie sicherstellen müssen, dass in dem Skript, das sie verwendet, bestimmte Methoden implementiert sind.
Die IEntity-Schnittstelle verfügt über eine Methode namens ApplyDamage, die später verwendet wird, um Feinden und unserem Spieler Schaden zuzufügen.
- Erstellen Sie ein neues Skript, nennen Sie es "SC_InterfaceManager" und fügen Sie den folgenden Code ein:
SC_InterfaceManager.cs
//Entity interafce
interface IEntity
{
void ApplyDamage(float points);
}
Einrichten eines Waffenmanagers
Ein Waffenmanager ist ein Objekt, das sich unter dem Hauptkameraobjekt befindet und alle Waffen enthält.
- Erstellen Sie ein neues GameObject und benennen Sie es "WeaponManager"
- Bewegen Sie den WeaponManager in die Hauptkamera des Spielers und ändern Sie seine Position auf (0, 0, 0).
- Hängen Sie das SC_WeaponManager-Skript an an "WeaponManager"
- Weisen Sie die Hauptkamera der Variablen „Spielerkamera“ in SC_WeaponManager zu
Ein Gewehr einrichten
- Ziehen Sie Ihr Waffenmodell per Drag-and-Drop in die Szene (oder erstellen Sie einfach einen Würfel und strecken Sie ihn, wenn Sie noch kein Modell haben).
- Skalieren Sie das Modell so, dass seine Größe relativ zu einer Spielerkapsel ist
In meinem Fall verwende ich ein maßgeschneidertes Gewehrmodell (BERGARA BA13):
- Erstellen Sie ein neues GameObject, nennen Sie es "Rifle" und verschieben Sie dann das Gewehrmodell darin
- Bewegen Sie das "Rifle"-Objekt innerhalb des "WeaponManager"-Objekts und platzieren Sie es wie folgt vor der Kamera:
Um den Objektausschnitt zu korrigieren, ändern Sie einfach die Nahausschnittebene der Kamera auf etwas Kleineres (in meinem Fall habe ich sie auf 0,15 eingestellt):
Viel besser.
- Hängen Sie das SC_Weapon-Skript an ein Gewehrobjekt an (Sie werden feststellen, dass es auch eine Audioquellenkomponente hinzugefügt hat, diese wird benötigt, um das Feuer abzuspielen und Audios neu zu laden).
Wie Sie sehen können, muss SC_Weapon vier Variablen zuweisen. Sie können Fire-Audio- und Reload-Audio-Variablen sofort zuweisen, wenn Ihr Projekt über geeignete Audio-Clips verfügt.
Die Bullet Prefab-Variable wird später in diesem Tutorial erklärt.
Im Moment weisen wir einfach die Fire-Point-Variable zu:
- Erstellen Sie ein neues GameObject, benennen Sie es in "FirePoint" um und verschieben Sie es in das Rifle Object. Platzieren Sie es direkt vor dem Lauf oder leicht innen, wie folgt:
- Weisen Sie die FirePoint-Transformation einer Fire-Point-Variablen bei SC_Weapon zu
- Weisen Sie das Gewehr einer Sekundärwaffenvariablen im SC_WeaponManager-Skript zu
Eine Maschinenpistole einrichten
- Duplizieren Sie das Gewehrobjekt und benennen Sie es in Maschinenpistole um
- Ersetzen Sie das darin enthaltene Waffenmodell durch ein anderes Modell (in meinem Fall verwende ich das maßgeschneiderte Modell von TAVOR X95)
- Verschieben Sie die Fire Point-Transformation, bis sie zum neuen Modell passt
- Weisen Sie Submachinegun einer Primärwaffenvariablen im SC_WeaponManager-Skript zu
Einrichten eines Bullet Prefab
Geschossfertigteile werden entsprechend der Feuerrate einer Waffe gespawnt und nutzen Raycast, um zu erkennen, ob sie etwas trifft und Schaden anrichtet.
- Erstellen Sie ein neues GameObject und benennen Sie es "Bullet"
- Fügen Sie die Trail-Renderer-Komponente hinzu und ändern Sie deren Zeitvariable auf 0,1.
- Stellen Sie die Breitenkurve auf einen niedrigeren Wert ein (z. B. Start 0,1 Ende 0), um eine Spur hinzuzufügen, die spitz aussieht
- Erstellen Sie ein neues Material, nennen Sie es „bullet_trail_material“ und ändern Sie seinen Shader in „Partikel/Additiv“.
- Weisen Sie einem Trail-Renderer ein neu erstelltes Material zu
- Ändern Sie die Farbe des Trail-Renderers in etwas anderes (z. B. Anfang: Helles Orange, Ende: Dunkleres Orange).
- Speichern Sie das Bullet-Objekt unter Prefab und löschen Sie es aus der Szene.
- Weisen Sie ein neu erstelltes Prefab (Drag & Drop aus der Projektansicht) der Prefab-Variablen Rifle und Submachinegun Bullet zu
Maschinenpistole:
Gewehr:
Die Waffen sind jetzt bereit.
Schritt 3: Erstellen Sie die Feind-KI
Die Feinde sind einfache Würfel, die dem Spieler folgen und angreifen, sobald sie nahe genug sind. Sie greifen in Wellen an, wobei jede Welle mehr Feinde zu eliminieren hat.
Feindliche KI einrichten
Unten habe ich zwei Varianten des Würfels erstellt (die linke ist für die lebendige Instanz und die rechte wird erzeugt, sobald der Feind getötet wird):
- Fügen Sie sowohl toten als auch aktiven Instanzen eine Rigidbody-Komponente hinzu
- Speichern Sie die tote Instanz in Prefab und löschen Sie sie aus der Szene.
Jetzt benötigt die lebendige Instanz ein paar weitere Komponenten, um durch das Spiellevel navigieren und dem Spieler Schaden zufügen zu können.
- Erstellen Sie ein neues Skript, nennen Sie es "SC_NPCEnemy" und fügen Sie dann den folgenden Code ein:
SC_NPCEnemy.cs
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
public class SC_NPCEnemy : MonoBehaviour, IEntity
{
public float attackDistance = 3f;
public float movementSpeed = 4f;
public float npcHP = 100;
//How much damage will npc deal to the player
public float npcDamage = 5;
public float attackRate = 0.5f;
public Transform firePoint;
public GameObject npcDeadPrefab;
[HideInInspector]
public Transform playerTransform;
[HideInInspector]
public SC_EnemySpawner es;
NavMeshAgent agent;
float nextAttackTime = 0;
// Start is called before the first frame update
void Start()
{
agent = GetComponent<NavMeshAgent>();
agent.stoppingDistance = attackDistance;
agent.speed = movementSpeed;
//Set Rigidbody to Kinematic to prevent hit register bug
if (GetComponent<Rigidbody>())
{
GetComponent<Rigidbody>().isKinematic = true;
}
}
// Update is called once per frame
void Update()
{
if (agent.remainingDistance - attackDistance < 0.01f)
{
if(Time.time > nextAttackTime)
{
nextAttackTime = Time.time + attackRate;
//Attack
RaycastHit hit;
if(Physics.Raycast(firePoint.position, firePoint.forward, out hit, attackDistance))
{
if (hit.transform.CompareTag("Player"))
{
Debug.DrawLine(firePoint.position, firePoint.position + firePoint.forward * attackDistance, Color.cyan);
IEntity player = hit.transform.GetComponent<IEntity>();
player.ApplyDamage(npcDamage);
}
}
}
}
//Move towardst he player
agent.destination = playerTransform.position;
//Always look at player
transform.LookAt(new Vector3(playerTransform.transform.position.x, transform.position.y, playerTransform.position.z));
}
public void ApplyDamage(float points)
{
npcHP -= points;
if(npcHP <= 0)
{
//Destroy the NPC
GameObject npcDead = Instantiate(npcDeadPrefab, transform.position, transform.rotation);
//Slightly bounce the npc dead prefab up
npcDead.GetComponent<Rigidbody>().velocity = (-(playerTransform.position - transform.position).normalized * 8) + new Vector3(0, 5, 0);
Destroy(npcDead, 10);
es.EnemyEliminated(this);
Destroy(gameObject);
}
}
}
- Erstellen Sie ein neues Skript, nennen Sie es "SC_EnemySpawner" und fügen Sie dann den folgenden Code ein:
SC_EnemySpawner.cs
using UnityEngine;
using UnityEngine.SceneManagement;
public class SC_EnemySpawner : MonoBehaviour
{
public GameObject enemyPrefab;
public SC_DamageReceiver player;
public Texture crosshairTexture;
public float spawnInterval = 2; //Spawn new enemy each n seconds
public int enemiesPerWave = 5; //How many enemies per wave
public Transform[] spawnPoints;
float nextSpawnTime = 0;
int waveNumber = 1;
bool waitingForWave = true;
float newWaveTimer = 0;
int enemiesToEliminate;
//How many enemies we already eliminated in the current wave
int enemiesEliminated = 0;
int totalEnemiesSpawned = 0;
// Start is called before the first frame update
void Start()
{
//Lock cursor
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
//Wait 10 seconds for new wave to start
newWaveTimer = 10;
waitingForWave = true;
}
// Update is called once per frame
void Update()
{
if (waitingForWave)
{
if(newWaveTimer >= 0)
{
newWaveTimer -= Time.deltaTime;
}
else
{
//Initialize new wave
enemiesToEliminate = waveNumber * enemiesPerWave;
enemiesEliminated = 0;
totalEnemiesSpawned = 0;
waitingForWave = false;
}
}
else
{
if(Time.time > nextSpawnTime)
{
nextSpawnTime = Time.time + spawnInterval;
//Spawn enemy
if(totalEnemiesSpawned < enemiesToEliminate)
{
Transform randomPoint = spawnPoints[Random.Range(0, spawnPoints.Length - 1)];
GameObject enemy = Instantiate(enemyPrefab, randomPoint.position, Quaternion.identity);
SC_NPCEnemy npc = enemy.GetComponent<SC_NPCEnemy>();
npc.playerTransform = player.transform;
npc.es = this;
totalEnemiesSpawned++;
}
}
}
if (player.playerHP <= 0)
{
if (Input.GetKeyDown(KeyCode.Space))
{
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
}
}
void OnGUI()
{
GUI.Box(new Rect(10, Screen.height - 35, 100, 25), ((int)player.playerHP).ToString() + " HP");
GUI.Box(new Rect(Screen.width / 2 - 35, Screen.height - 35, 70, 25), player.weaponManager.selectedWeapon.bulletsPerMagazine.ToString());
if(player.playerHP <= 0)
{
GUI.Box(new Rect(Screen.width / 2 - 85, Screen.height / 2 - 20, 170, 40), "Game Over\n(Press 'Space' to Restart)");
}
else
{
GUI.DrawTexture(new Rect(Screen.width / 2 - 3, Screen.height / 2 - 3, 6, 6), crosshairTexture);
}
GUI.Box(new Rect(Screen.width / 2 - 50, 10, 100, 25), (enemiesToEliminate - enemiesEliminated).ToString());
if (waitingForWave)
{
GUI.Box(new Rect(Screen.width / 2 - 125, Screen.height / 4 - 12, 250, 25), "Waiting for Wave " + waveNumber.ToString() + " (" + ((int)newWaveTimer).ToString() + " seconds left...)");
}
}
public void EnemyEliminated(SC_NPCEnemy enemy)
{
enemiesEliminated++;
if(enemiesToEliminate - enemiesEliminated <= 0)
{
//Start next wave
newWaveTimer = 10;
waitingForWave = true;
waveNumber++;
}
}
}
- Erstellen Sie ein neues Skript, nennen Sie es "SC_DamageReceiver" und fügen Sie dann den folgenden Code ein:
SC_DamageReceiver.cs
using UnityEngine;
public class SC_DamageReceiver : MonoBehaviour, IEntity
{
//This script will keep track of player HP
public float playerHP = 100;
public SC_CharacterController playerController;
public SC_WeaponManager weaponManager;
public void ApplyDamage(float points)
{
playerHP -= points;
if(playerHP <= 0)
{
//Player is dead
playerController.canMove = false;
playerHP = 0;
}
}
}
- Hängen Sie das SC_NPCEnemy-Skript an eine aktive Feindinstanz an (Sie werden feststellen, dass es eine weitere Komponente namens NavMesh Agent hinzugefügt hat, die zum Navigieren im NavMesh benötigt wird)
- Weisen Sie das kürzlich erstellte tote Instanz-Prefab der Variable „Npc Dead Prefab“ zu
- Erstellen Sie für den Fire Point ein neues GameObject, verschieben Sie es in die lebende Feindinstanz, platzieren Sie es leicht vor der Instanz und weisen Sie es dann der Fire Point-Variablen zu:
- Speichern Sie abschließend die lebendige Instanz in Prefab und löschen Sie sie aus der Szene.
Feind-Spawner einrichten
Kommen wir nun zu SC_EnemySpawner. Dieses Skript bringt Feinde in Wellen hervor und zeigt außerdem einige UI-Informationen auf dem Bildschirm an, z. B. Spieler-HP, aktuelle Munition, wie viele Feinde in einer aktuellen Welle übrig sind usw.
- Erstellen Sie ein neues GameObject und benennen Sie es "_EnemySpawner"
- Hängen Sie das SC_EnemySpawner-Skript daran an
- Weisen Sie die neu erstellte Feind-KI der Enemy Prefab-Variable zu
- Weisen Sie die Textur unten der Variablen „Crosshair Texture“ zu
- Erstellen Sie ein paar neue GameObjects, platzieren Sie sie in der Szene und weisen Sie sie dann dem Spawn Points-Array zu
Sie werden feststellen, dass noch eine letzte Variable zugewiesen werden muss, nämlich die Player-Variable.
- Hängen Sie das SC_DamageReceiver-Skript an eine Player-Instanz an
- Ändern Sie das Player-Instanz-Tag in "Player"
- Weisen Sie Player Controller- und Weapon Manager-Variablen in SC_DamageReceiver zu
- Weisen Sie eine Player-Instanz einer Player-Variablen in SC_EnemySpawner zu
Und schließlich müssen wir das NavMesh in unserer Szene backen, damit die feindliche KI navigieren kann.
Vergessen Sie außerdem nicht, jedes statische Objekt in der Szene als Navigationsstatisch zu markieren, bevor Sie NavMesh backen:
- Gehen Sie zum NavMesh-Fenster (Fenster -> KI -> Navigation), klicken Sie auf die Registerkarte Backen und dann auf die Schaltfläche Backen. Nachdem das NavMesh gebacken wurde, sollte es etwa so aussehen:
Jetzt heißt es Play drücken und testen:
Alles funktioniert wie erwartet!