Unity3d FPS Tutorial, czyli tworzymy własną grę FPS od podstaw z wykorzystaniem silnika Unity3d.

Temat: Przybliżenie i dziury po kulach

Spis treści

Jeżeli nie używałeś do tej pory Unity3d:

#0 – Podstawy Podstaw

FPS Tutorial:

#1 – Tworzenie nowego projektu i narzędzie terenu

#2 – Sterowanie postacią

#3 – Życie, pancerz i wytrzymałość postaci

#4 – Regeneracja życia i energii. Efekty trafienia

#5 – Kamera z bronią i strzelanie

#6 – Przeładowanie i amunicja

#7 – Zbieranie przedmiotów

#8 – Druga broń

#9 – Rzut granatem i seria z karabinu

– Przybliżenie i dziury po kulach

#11 – Przeciwnik z prostym AI, sztuczna inteligencja

#12 – Animacja postaci przeciwnika. Animator

#13 – Menu główne gry. GUI

#14 – Ostatnie szlify i budujemy projekt

Teoria

W dzisiejszym odcinku, dopieścimy naszą broń i zostawimy już temat dopracowywania elementów. Co zrobimy dzisiaj? Dwa elementy, finalizujące broń. Zoom po naciśnięciu prawego klawisza myszki, oraz efekt dziur po kulach.

Do wykonania tej części, potrzeba tylko tekstury dziury po kuli. Ja używam tego:

Dziura po kuli
Dziura po kuli

Przycelowanie – zoom

Tutaj sprawa jest prosta, bo wystarczy manewrować odpowiednim elementem kamery, by uzyskać efekt przybliżenia. Ponownie operować będziemy na skrypcie Shooting.cs. Żeby nic się nie posypało zapisujemy sobie zmienne pomocnicze:

private float zoomFieldOfView = 40.0f;
private float defaultFieldOfView = 60.0f;

Nie są to wartości znikąd. Jeśli wjedziesz do Unity i zaznaczysz obiekt Main Camera, to w komponencie Camera znajdziesz wartość: Field Of View. Zmienna defaultFieldOfView, to wartość, która jest ustawiona domyślnie, druga wartość jest odpowiednio pomniejszona, by uzyskać efekt przybliżenia. Wartość zooma najlepiej dobrać doświadczalnie, czyli ustawiać jakieś wartości i zobaczyć, jaka najlepiej wygląda.

Teraz, przechodzimy do funkcji Update, gdzie dodajemy taki kodzik:

if(gameObject.GetComponentInParent<Camera>() is Camera) {
	Camera cam = gameObject.GetComponentInParent<Camera>();
	if(Input.GetButton("Fire2")) {
		if(cam.fieldOfView > zoomFieldOfView) {
			cam.fieldOfView--;
		}
	} else {
		if(cam.fieldOfView < defaultFieldOfView) {
			cam.fieldOfView++;
		}
	}
}

Przeanalizujmy linia po linii. Pierwszy if, sprawdza czy rodzin naszego obiektu (w tym wypadku broni) posiada komponent Camera. Jeżeli tak jest, zapisujemy go sobie do zmiennej cam. Następnie rozważamy dwa przypadki, gdy naciśnięty jest przycisk Fire2 (czyli prawy klawisz myszki – można to zmienić w ustawieniach Input: [Edit -> Project Settings… -> Input]). Jeżeli przycisk jest naciśnięty, chcemy przybliżyć, jeśli nie, oddalić. Robimy to odpowiednio zmniejszając lub zwiększając fieldOfView, do którego mamy łatwy dostęp dzięki zmiennej cam.

Zostaje kwestia ifów wewnątrz. Są one po to, żebyśmy mogli zmniejszać/zwiększać parametr co 1, dzięki czemu uzyskamy efekt płynnego przejścia. Tutaj swoje zastosowanie mają nasze zmienne pomocnicze, dzięki którym nie przekroczymy ustalonych wartości.

Tyle! Zoom działa!

Dziury po kulach

Póki jesteśmy w skrypcie, możemy najpierw dodać kod. Znów zaczynamy od zmiennej pomocniczej:

public GameObject bulletHole;

Będzie to prefabrykant naszej dziury. Teraz samo pojawienie dziury, dodajemy go w warunku trafienia w ścianę po strzale:

GameObject go;
go = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal)) as GameObject; 
Destroy(go, 5);

Tworzymy sobie obiekt, dzięki znanej nam funkcji instantinate. Ciekawe jest tutaj jedynie użycie hit.point i hit.normal do określenia rotacji i położenia dziury. Dzięki temu, dziura pojawi się tam, gdzie padł promień strzału broni, oraz zawsze będzie ustawiona wektorem Y prostopadle do powierzchni. Daje nam to tyle, że dziura nie pojawi się na ścianie np. bokiem. W sumie tyle kodu, czas przygotować dziurę. Wracamy do Unity.

Schemat jest prosty. Tworzymy sobie Cube: [GameObject -> 3D Object -> Cube] – mój się nazywa Hole.  Spłaszczamy go, ustawiając Scale.y na 0. Nakładamy teraz na niego teksturę dziury. Zmieniamy jeszcze Shader na Transparent/Bumped Diffuse, dzięki temu dziura będzie miała przeźroczyste tło, jeśli takowe będzie posiadał plik.

Poprawny obiekt Hole
Poprawny obiekt Hole

Teraz tworzymy pusty GameObject [GameObject -> Create Empty] – mój się nazywa BulletHole. Obiekt Hole ustawiamy jako child pustego gameObjectu. Ostatni krok, to ustawienie, dla obiektu Hole wartości transform position y na 0.1. Dzięki temu manewrowi, dziura zawsze będzie na wierzchu.

Zostało już tylko zrobić z obiektu BulletHole prefabrykant (przenosimy go do panelu Project), a następnie dodać ten prefabrykant do odpowiedniego pola w obiektach broni (te przygotowane wcześniej publiczne pola).

Jeżeli po pojawieniu się, rozmiar dziury jest za duży, wystarczy zmniejszyć parametr Scale dla komponentu Transform, naszego prefabrykantu.

Podsumowanie

Dziś nie zrobiliśmy wiele, ale dzięki temu gra uzyskała nieco uroku. Tak jak wspominałem, dalej nie będziemy się już bawić za bardzo tym co mamy, a zrobimy coś nowego.

Z racji, że w skrypcie Shooting.cs mieszaliśmy już bardzo dużo, udostępniam pełny skrypt:

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class Shooting : MonoBehaviour 
{
	public Texture2D crosshairTexture;
	public AudioClip pistolShot;
	public AudioClip reloadSound;
	public int maxAmmo = 200;
	public int clipSize = 10;
	public GUIText ammoText;
	public GUIText reloadText;
	public float reloadTime = 3.0f;
	public bool automatic = false;
	public float shotDelay = 0.5f;
	public GameObject bulletHole; 

	private int currentAmmo = 30;
	private int currentClip;
	private Rect position;   
	private float range = 200.0f;
	private GameObject pistolSparks; 
	private Vector3 fwd;
	private RaycastHit hit;
	private bool isReloading = false;
	private float shotDelayCounter = 0.0f;
	private float zoomFieldOfView = 40.0f;
	private float defaultFieldOfView = 60.0f;


	private float timer = 0.0f;

	void Start()
	{
		Screen.showCursor = false;
		position = new Rect((Screen.width - crosshairTexture.width) / 2,
		                    (Screen.height - crosshairTexture.height) /2,
		                    crosshairTexture.width,
		                    crosshairTexture.height);
		
		pistolSparks = GameObject.Find("Sparks");
		pistolSparks.particleEmitter.emit = false;
		audio.clip = pistolShot;
		currentClip = clipSize;
	}

	void Update () 
	{

		if(shotDelayCounter > 0) {
			shotDelayCounter -= Time.deltaTime;
		}
		Transform tf = transform.parent.GetComponent<Transform>();
		fwd = tf.TransformDirection(Vector3.forward);

		if(((Input.GetButtonDown("Fire1") && currentClip == 0) || Input.GetButtonDown("Reload")) && currentClip < clipSize) {
			if(currentAmmo > 0) {
				audio.clip = reloadSound;
				audio.Play();
				isReloading = true;
			}
		}

		if(isReloading) {
			timer += Time.deltaTime;
			if(timer >= reloadTime) {
				int needAmmo = clipSize - currentClip;
				
				if(currentAmmo >= needAmmo) {
					currentClip = clipSize;
					currentAmmo -= needAmmo;
				} else {
					currentClip += currentAmmo;
					currentAmmo = 0;
				}
				
				audio.clip = pistolShot;
				isReloading = false;
				timer = 0.0f;
			}
		}

		if(currentClip > 0 && !isReloading) {
			if((Input.GetButtonDown("Fire1") || (Input.GetButton("Fire1") && automatic)) && shotDelayCounter <= 0) {
				shotDelayCounter = shotDelay;
				currentClip--;
				pistolSparks.particleEmitter.Emit();
				audio.Play();
				
				if (Physics.Raycast(tf.position, fwd, out hit)) {
					if(hit.transform.tag == "Enemy" && hit.distance < range) {
						Debug.Log ("Trafiony przeciwnik");
						
					} else if(hit.distance < range) {
						GameObject go;
						go = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal)) as GameObject; 
						Destroy(go, 5);
						Debug.Log ("Trafiona Sciana");
					}
				}
			}
		}

		if(gameObject.GetComponentInParent<Camera>() is Camera) {
			Camera cam = gameObject.GetComponentInParent<Camera>();
			if(Input.GetButton("Fire2")) {
				if(cam.fieldOfView > zoomFieldOfView) {
					cam.fieldOfView--;
				}
			} else {
				if(cam.fieldOfView < defaultFieldOfView) {
					cam.fieldOfView++;
				}
			}
		}

	}

	void OnGUI()
	{
		GUI.DrawTexture(position, crosshairTexture);
		ammoText.pixelOffset = new Vector2(-Screen.width / 2 + 100, -Screen.height / 2 + 30);
		ammoText.text = currentClip + " / " + currentAmmo;

		if(currentClip == 0) {
			reloadText.enabled = true;
		} else {
			reloadText.enabled = false;
		}
	}	

	void addAmmo(Vector2 data) 
	{
		int ammoToAdd = (int)data.x;

		if(maxAmmo - currentAmmo >= ammoToAdd) {
			currentAmmo += ammoToAdd;
		} else {
			currentAmmo = maxAmmo;
		}
	}

	public bool canGetAmmo()
	{
		if(currentAmmo == maxAmmo) {
			return false;
		}
		return true;
	}

}

 

Poprzednia część <- #9 – Rzut granatem i seria z karabinu

Następna część-> #11 – Przeciwnik z prostym AI, sztuczna inteligencja