Cara Membuat FPS Dengan Dukungan AI di Unity
First-person shooter (FPS) adalah subgenre game penembak yang pemainnya dikontrol dari sudut pandang orang pertama.
Untuk membuat game FPS di Unity kita memerlukan pengontrol pemain, serangkaian item (dalam hal ini senjata), dan musuh.
Langkah 1: Buat Pengontrol Pemain
Disini kita akan membuat controller yang akan digunakan oleh player kita.
- Buat Objek Game baru (Objek Game -> Buat Kosong) dan beri nama "Player"
- Buat Kapsul baru (Objek Game -> Objek 3D -> Kapsul) dan pindahkan ke dalam Objek "Player"
- Lepaskan komponen Capsule Collider dari Capsule dan ubah posisinya menjadi (0, 1, 0)
- Pindahkan Kamera Utama ke dalam Objek "Player" dan ubah posisinya menjadi (0, 1.64, 0)
- Buat skrip baru, beri nama "SC_CharacterController" dan tempel kode di bawah ini di dalamnya:
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);
}
}
}
- Lampirkan skrip SC_CharacterController ke Objek "Player" (Anda akan melihat bahwa ia juga menambahkan komponen lain yang disebut Pengontrol Karakter, mengubah nilai tengahnya menjadi (0, 1, 0))
- Tetapkan Kamera Utama ke variabel Kamera Pemain di SC_CharacterController
Pengontrol Player sekarang siap:
Langkah 2: Buat Sistem Senjata
Sistem senjata pemain akan terdiri dari 3 komponen: Manajer Senjata, skrip Senjata, dan skrip Peluru.
- Buat skrip baru, beri nama "SC_WeaponManager" dan tempel kode di bawah ini di dalamnya:
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;
}
}
}
- Buat skrip baru, beri nama "SC_Weapon" dan tempel kode di bawah ini di dalamnya:
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);
}
}
- Buat skrip baru, beri nama "SC_Bullet" dan tempel kode di bawah ini di dalamnya:
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;
}
}
Sekarang, Anda akan melihat bahwa skrip SC_Bullet memiliki beberapa kesalahan. Itu karena ada satu hal terakhir yang harus kita lakukan, yaitu mendefinisikan antarmuka IEntity.
Antarmuka di C# berguna ketika Anda perlu memastikan bahwa skrip yang menggunakannya, telah menerapkan metode tertentu.
Antarmuka IEntity akan memiliki satu metode yaitu ApplyDamage, yang nantinya akan digunakan untuk memberikan kerusakan pada musuh dan pemain kita.
- Buat skrip baru, beri nama "SC_InterfaceManager" dan tempel kode di bawah ini di dalamnya:
SC_InterfaceManager.cs
//Entity interafce
interface IEntity
{
void ApplyDamage(float points);
}
Menyiapkan Manajer Senjata
Manajer senjata adalah sebuah Objek yang akan berada di bawah Objek Kamera Utama dan akan berisi semua senjata.
- Buat GameObject baru dan beri nama "WeaponManager"
- Pindahkan WeaponManager ke dalam Kamera Utama Pemain dan ubah posisinya menjadi (0, 0, 0)
- Lampirkan skrip SC_WeaponManager ke "WeaponManager"
- Tetapkan Kamera Utama ke variabel Kamera Pemain di SC_WeaponManager
Menyiapkan Senapan
- Seret dan lepas model senjata Anda ke dalam adegan (atau cukup buat Kubus dan rentangkan jika Anda belum memiliki modelnya).
- Skalakan model sehingga ukurannya relatif terhadap Kapsul Pemain
Dalam kasus saya, saya akan menggunakan model Senapan yang dibuat khusus (BERGARA BA13):
- Buat GameObject baru dan beri nama "Rifle" lalu pindahkan model senapan ke dalamnya
- Pindahkan Objek "Rifle" ke dalam Objek "WeaponManager" dan letakkan di depan Kamera seperti ini:
Untuk memperbaiki objek yang terpotong, cukup ubah bidang dekat kamera menjadi lebih kecil (dalam kasus saya, saya menyetelnya ke 0,15):
Jauh lebih baik.
- Lampirkan skrip SC_Weapon ke Objek Senapan (Anda akan melihat bahwa skrip ini juga menambahkan komponen Sumber Audio, ini diperlukan untuk menembakkan api dan memuat ulang audio).
Seperti yang Anda lihat, SC_Weapon memiliki 4 variabel untuk ditetapkan. Anda dapat langsung menetapkan variabel audio Api dan Muat Ulang audio jika Anda memiliki Klip Audio yang sesuai di proyek Anda.
Variabel Bullet Prefab akan dijelaskan nanti di tutorial ini.
Untuk saat ini, kami hanya akan menetapkan variabel Titik api:
- Buat GameObject baru, ganti namanya menjadi "FirePoint" dan pindahkan ke dalam Rifle Object. Tempatkan tepat di depan laras atau sedikit ke dalam, seperti ini:
- Tetapkan FirePoint Transform ke variabel Fire point di SC_Weapon
- Tetapkan Senapan ke variabel Senjata Sekunder dalam skrip SC_WeaponManager
Menyiapkan Senapan Mesin Ringan
- Gandakan Objek Senapan dan ganti namanya menjadi Submachinegun
- Ganti model senjata di dalamnya dengan model lain (Dalam kasus saya, saya akan menggunakan model TAVOR X95 yang dibuat khusus)
- Pindahkan transformasi Fire Point hingga sesuai dengan model baru
- Tetapkan Submachinegun ke variabel Senjata Utama dalam skrip SC_WeaponManager
Menyiapkan Prefab Peluru
Cetakan peluru akan muncul sesuai dengan laju tembakan Senjata dan akan menggunakan Raycast untuk mendeteksi apakah peluru tersebut mengenai sesuatu dan menimbulkan kerusakan.
- Buat GameObject baru dan beri nama "Bullet"
- Tambahkan komponen Trail Renderer ke dalamnya dan ubah variabel Time menjadi 0.1.
- Atur kurva Lebar ke nilai yang lebih rendah (mis. Mulai 0,1 berakhir 0), untuk menambahkan jejak yang terlihat lancip
- Buat Material baru dan beri nama bullet_trail_material dan ubah Shadernya menjadi Particles/Additive
- Tetapkan materi yang baru dibuat ke Trail Renderer
- Ubah Warna Trail Renderer menjadi sesuatu yang berbeda (misal: Mulai: Oranye Terang Akhir: Oranye Gelap)
- Simpan Objek Bullet ke Prefab dan hapus dari Scene.
- Tetapkan Prefab yang baru dibuat (drag & drop dari tampilan Proyek) ke variabel Prefab Senapan dan Peluru Submachinegun
Senapan mesin ringan:
Senapan:
Senjatanya sekarang sudah siap.
Langkah 3: Buat AI Musuh
Musuhnya berupa Kubus sederhana yang mengikuti Pemain dan menyerang setelah mereka cukup dekat. Mereka akan menyerang dalam gelombang, dan setiap gelombang mempunyai lebih banyak musuh yang harus dilenyapkan.
Menyiapkan AI Musuh
Di bawah ini saya telah membuat 2 variasi Kubus (Yang Kiri untuk instance hidup dan yang Kanan akan muncul setelah musuh terbunuh):
- Tambahkan komponen Rigidbody ke instance mati dan hidup
- Simpan Instance Mati ke Prefab dan hapus dari Scene.
Sekarang, instance hidup memerlukan beberapa komponen lagi untuk dapat menavigasi level permainan dan menimbulkan kerusakan pada Pemain.
- Buat skrip baru dan beri nama "SC_NPCEnemy" lalu tempel kode di bawah ini di dalamnya:
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);
}
}
}
- Buat skrip baru, beri nama "SC_EnemySpawner" lalu tempel kode di bawah ini di dalamnya:
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++;
}
}
}
- Buat skrip baru, beri nama "SC_DamageReceiver" lalu tempel kode di bawah ini di dalamnya:
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;
}
}
}
- Lampirkan skrip SC_NPCEnemy ke instance musuh yang aktif (Anda akan melihatnya menambahkan komponen lain yang disebut Agen NavMesh, yang diperlukan untuk menavigasi NavMesh)
- Tetapkan prefab instance mati yang baru dibuat ke variabel NPC Dead Prefab
- Untuk Fire Point, buat GameObject baru, pindahkan ke dalam instance musuh yang masih hidup dan letakkan sedikit di depan instance tersebut, lalu tetapkan ke variabel Fire Point:
- Terakhir, Simpan instance yang masih hidup ke Prefab dan hapus dari Scene.
Menyiapkan Spawner Musuh
Sekarang mari beralih ke SC_EnemySpawner. Skrip ini akan memunculkan musuh dalam gelombang dan juga akan menampilkan beberapa informasi UI di layar, seperti HP Pemain, Amunisi saat ini, berapa banyak Musuh yang tersisa dalam gelombang saat ini, dll.
- Buat GameObject baru dan beri nama "_EnemySpawner"
- Lampirkan skrip SC_EnemySpawner ke sana
- Tetapkan AI musuh yang baru dibuat ke variabel Prefab Musuh
- Tetapkan tekstur di bawah ini ke variabel Crosshair Texture
- Buat beberapa GameObjects baru dan letakkan di sekitar Scene lalu tetapkan mereka ke array Spawn Points
Anda akan melihat bahwa ada satu variabel terakhir yang tersisa untuk ditetapkan yaitu variabel Pemain.
- Lampirkan skrip SC_DamageReceiver ke instance Player
- Ubah tag instance Player menjadi "Player"
- Tetapkan variabel Pengontrol Pemain dan Manajer Senjata di SC_DamageReceiver
- Tetapkan instance Player ke variabel Player di SC_EnemySpawner
Dan terakhir, kita harus memasang NavMesh di adegan kita sehingga AI musuh dapat bernavigasi.
Juga, jangan lupa untuk menandai setiap Objek statis di Scene sebagai Navigasi Statis sebelum membuat NavMesh:
- Masuk ke jendela NavMesh (Window -> AI -> Navigation), klik pada tab Bake lalu klik tombol Bake. Setelah NavMesh dipanggang, tampilannya akan seperti ini:
Sekarang saatnya menekan Mainkan dan mengujinya:
Semuanya berfungsi seperti yang diharapkan!