Goals - Unity / FPS microgame code를 분석한다
FPS microgame / Assets / FPS / Scripts / |
||||
AI / |
||||
DetectionModule.cs | EnemyController.cs | |||
EnemyManager.cs | EnemyMobile.cs | |||
EnemyTurret.cs | FollowPlayer.cs | |||
NavigationModule.cs | PatrolPath.cs | |||
Editor / | ||||
MiniProfiler.cs | PrefabReplacerEditor.cs | |||
ShaderBuildStripping.cs | UITableEditor.cs | |||
Game / | ||||
Actor.cs | T | AudioUtility.cs | T | |
ConstantRotation.cs | T | DebugUtility.cs | T | |
Event.cs | T | GameConstants.cs | T | |
IgnoreHeatMap.cs | T | IgnoreHitDetection.cs | T | |
MeshCombiner.cs | MeshCombineUtility.cs | |||
MinMaxParameters.cs | T | PrefabReplacer.cs | ||
PrefabReplacerOninstance.cs | TimedSelfDestruct.cs | T | ||
Game / Managers / | ||||
ActorsManager.cs | T | AudioManger.cs | T | |
EventManager.cs | T | GameFlowManager.cs | T | |
ObjectiveManager.cs | T | |||
Game / Shared / | ||||
Damageable.cs | T | DamegeArea.cs | T | |
Destructable.cs | T | Health.cs | T | |
Objective.cs | T | ProjectileBase.cs | T | |
WeaponController.cs | T | |||
Gameplay | ||||
AmmoPickup.cs | T | ChargedProjectileEffectsHandler.cs | T | |
ChargedWeaponEffectsHandler.cs | T | HealthPickup.cs | T | |
Jetpack.cs | T | JetpackPickup.cs | T | |
OverheatBehavior.cs | T | Pickup.cs | T | |
PlayerCharacterController.cs | PositionBobbing.cs | skip | ||
ProjectileChargeParameters.cs | T | ProjectileStandard.cs | T | |
TeleportPlayer.cs | WeaponFuelCellHandler.cs | T | ||
WeaponPickup.cs | T | |||
Gameplay / Managers / | ||||
PlayerInputHandler.cs | PlayerWeaponsManager.cs | |||
Gameplay / Objectives / | ||||
ObjectiveKillEnemies.cs | T | ObjectivePickupItem.cs | T | |
ObjectiveReachPoint.cs | T | |||
UI / | ||||
AmmoCounter.cs | Compass.cs | |||
CompassElement.cs | CompassMarker.cs | |||
CrosshairManager.cs | DisplayMessage.cs | |||
DisplayMessageManger.cs | EnemyCounter.cs | |||
FeedbackFlashHUD.cs | FillBarColorChange.cs | |||
FramerateCounter.cs | InGameMenuManager.cs | |||
JetpackCounter.cs | LoadSceneButton.cs | |||
MenuNavigation.cs | NotificationHUDManager.cs | |||
NotificationToast.cs | ObjectiveHUDManager.cs | |||
ObjectiveToast.cs | PlayerHealthBar.cs | |||
StanceHUD.cs | TakeScreenshot.cs | |||
ToggleGameObjectButton.cs | UITable.cs | |||
WeaponHUDManager.cs | WorldspaceHealthBar.cs |
Rule
- 간단하게 작성한다.
- 멤버 표 작성은 하되 확신은 없으므로 언제든지 중단한다.
- 코드블럭은 시각적으로 효과적이라는 판단하에서만, 하더라도 일부만
현재 20250420 초기에 기본 구성 매니저 -> 게임 기본 구성 클래스까지
보여지는 건, 적 컨트롤러, 메시 조작, 에디터 부가 기능, 유저 상호작용.
Daily
2024/05 |
||
24 | T | EventManger.cs(진행) , Event.cs |
25 | T | EventManger.cs |
2025/04 | ||
11 | T | EventManger.cs 재검토 |
14 | T | Event 재검토 및 GameFlowManager.cs,ActorsManager.cs, Actor.cs, AudioManger.cs |
- | ObjectiveManager.cs, Objective.cs | |
15 | - | ObjectiveKillEnemies.cs, ObjectivePickupItem.cs, ObjectiveReachPoint.cs, Health.cs |
16 | T | Damageable.cs, Destructable.cs, WeaponController.cs, ProjectileBase.cs |
17 | T | ProjectileStandard.cs 그 외 기타 |
21 | - | 잠깐 중간 정리, Pickup.cs, HealthPickup.cs, AmmoPickup.cs, WeaponPickup.cs, JetpackPickup.cs |
- | ChargedProjectileEffectsHandler.cs, ChargedWeaponEffectsHandler.cs, WeaponFuelCellHandler.cs | |
22 | - | ProjectileChargeParameters.cs, OverheatBehavior.cs |
Remember
EventManger.cs, 딕션어리 활용, 람다식 활용
Event.cs 빈 클래스로도 형상화, 빈 클래스 사용시 메서드의 인자의 종류에 따라 메서드 형상화
GameFlowManager.cs 간단한 메서드를 어떻게 형상화 해야할지 => EndGame(true);
ActorsManager.cs 마치 엔티티 버전의 서비스 로케이터
Actor.cs 의외로 간단한 Actor, 아마 컴포넌트를 활용하기 때문일 것
AudioManager.cs, AudioUtility.cs 유니티 에디터의 audio mixer와 관계
ObjectiveManage.cs, Objective.cs 보통 매니저의 등록 메서드를 호출할거 같은데, 여기서는 매니저가 구독하는 형태로 등록
Health.cs 상태를 알리는 것으로 상태에 대한 동작과 구별됨
Damageable.cs, Destructable.cs 컴포넌트 자체도 성질처럼 사용할 수 있다.
WeaponController.cs 발사 동작 감지 -> 발사 시도(유효 확인) -> 발사 동작 핸들러(모든 종류의 총이 한 메서드에서.) 이러한 구조, 반드시는 아니다.P
ProjectileBase.cs 아마 투사체가 시스템적으로 기본적으로 가져야 한다고 생각하는 속성을 선언해 놓았다.
DamageArea.cs 범위와 적용할 콜리더 다루는 방법
ProjectileStandard.cs 카메라 시야에 대한 보정을 하는 방법
IgnoreHitDetection.cs, IgnoreHeatMap.cs 컴포넌트 자체도 플래그처럼 사용할 수 있다.
MinMaxParameters.cs 열거형과 유니티 에디터에서 나오느 설정바
DebugUtility.cs 전처리 명령어
GameConstants.cs 에디터 input manager와 관련
TimedSelfDestruct.cs
ObjectiveKillEnemies.cs, ObjectivePickupItem.cs, ObjectiveReachPoint.cs
ConstantRotation.cs
GameConstants.cs
Game / Managers / EventManger.cs
Game / Event.cs와 엮어서 볼 것.
딜리게이트를 적용한 제너릭을 사용할 때, 왜 람다식을 사용했는지 유의할것, 그러면 왜 lookup이 있어야 하는지 알 수 있음.
namespace Unity.FPS.Game
{
public class GameEvent
{
}
// A simple Event System that can be used for remote systems communication
public static class EventManager
{
static readonly Dictionary<Type, Action<GameEvent>> s_Events =
new Dictionary<Type, Action<GameEvent>>();
static readonly Dictionary<Delegate, Action<GameEvent>> s_EventLookups =
new Dictionary<Delegate, Action<GameEvent>>();
public static void AddListener<T>(Action<T> evt) where T : GameEvent
public static void RemoveListener<T>(Action<T> evt) where T : GameEvent
public static void Broadcast(GameEvent evt)
public static void Clear()
}
}
class GameEvent
- GameEvent의 틀을 정해주기 위한 클래스이다. interface라도 보이도 된다. 재미있는게, 아무런 멤버가 없더라도 GameEvent의 클래스 명 자체로 GameEvent을 형상화 한다.
class EventManager
- 정적 클래스 선언하고 정적 메서드만 가지며 항상 접근가능하다.
※우선, 코드 특성상, 딱히 이벤트 클래스 내부에 멤버가 없더라도. 이벤트 클래스를 가짐으로써 어떤 이벤트를 추적하는 지 나타냄, 메서드가 어떤 이벤트를 추적하는 지 단순 형상화 하기 위해 인자로 사용함.
//선언
public class AllObjectivesCompletedEvent : GameEvent { }
//활용
void OnAllObjectivesCompleted(AllObjectivesCompletedEvent evt) => EndGame(true);
//사용예제
EventManager.AddListener<AllObjectivesCompletedEvent>(OnAllObjectivesCompleted);
Dictionary<Type, Action<GameEvent>> s_Events
- Event를 상속받은 자식 클래스, 그 이벤트 클래스를 인자로 같은 메서드 딜리게이트
- 메서드 인자가 곧 추적하는 이벤트를 나타내기에, 이벤트 타입으로 해당 딜리게이트를 가져올 수 있음.
- 딕션어리와 타입으로 채널처럼 옵저버 패턴을 사용함
Dictionary<Delegate, Action<GameEvent>> s_EventLooksups
- 먼저 AddListener<T>를 봐야함, evt 를 바로 등록하지 않고 (e) => evt((T)e) 람다식으로 새로운 딜리게이트를 선언하여 등록함
- 이때 구독 취소를 위해서는 람다식으로 만든 딜리게이트를 유다시 조회할 방법이 필요함.
- 따라서 딜리게이트 evt로 람다식을 다시 가져올 수 있게 딕션어리를 만듬
void AddListener<T>(Action<T> evt) where T : GameEvent
//point
Action<GameEvent> newAction = (e) => evt((T) e);
if (s_Events.TryGetValue(typeof(T), out Action<GameEvent> internalAction))
s_Events[typeof(T)] = internalAction += newAction;
//활용 메서드
s_EventLookups.ContainsKey(evt)
s_Events.TryGetValue(typeof(T), out Action<GameEvent> internalAction)
- 코드 설계상 어떤 매서드의 인수가 GameEvent 자식 클래스 타입이면 그 이벤트를 추적하려는 매서드
- Action<T> evt는 그러한 메서드를 나타냄
- 이때 evt 를 바로 등록하지 않고 (e) => evt((T)e) 으로 하는 이유는 s_Events 가 Action<GameEvent> 타입을 받아야하는데, evt는 Action<T> 타입이기 때문임
- (e) => evt((T)e) 도 Action<T> 타입이 아니냐고 생각할 수 있지만, 이건 전적으로 전처리가 판단함.
- 즉 (e) => evt((T)e) 에서 e를 GameEvent라 판단하고 Action<GameEvent> 딜리게이트를 생성해줌
- where T : GameEvent 의 경우 타입 자체를 결정하는 데에 영향은 없다.
- 여담으로 프로그래머가 작성한 메서드가 통째로 id가 되고 그 id로 람다식을 구성하고 관리하는 느낌이다.
- 구독전에 s_EventLookups.ContainsKey(evt)) 를 통해 이미 구독된 여부를 확인한다.
- s_Events.TryGetValue(typeof(T), out Action<GameEvent> internalAction) 를 통해 조회와 같이 값을 반환 받는다.
- out은 return 과 다르게 인자 형태로 값을 전달 받는다.
- 이 방법이 최적화가 잘 되어 있다.
void RemoveListener<T>(Action<T> evt) where T : GameEvent
//활용 메서드
s_Events.Remove(typeof(T));
- s_EventLookups를 사용해서 구독 해제할 람다식을 조회하고 제거한다.
Delegate를 사용해 해당하는 Action<GameEvent> newAction를 찾고 제거해준다.
void Broadcast(GameEvent evt)
if (s_Events.TryGetValue(evt.GetType(), out var action))
action.Invoke(evt);
- 등록된 옵저버에게 invoke한다. 코드상 등록된 옵저버가 없을 수도 있기 때문에 확인을 해야한다.
- 추적 메서드에 옵저버를 다는 것이 아니라, GamEvent의 자식 클래스 형에 따라 브로드캐스트 하듯이 바뀌었다.
void Clear()
//활용 메서드
s_Events.Clear();
- 자원 반환
Action A;
A -= A; //null
2024.05.25 - [Experiment/Unity] - Unity, C#, Action 과 +=, -=
Unity, C#, Action 과 +=, -=
using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class DelegateTest : MonoBehaviour{ public Action A; public Action B; public Action C; void Start() { A = () => Debug.Log("Action A"); B = () => Debug.Log("Acti
roaring-stretching-thought-wood.tistory.com
Ramda
2025.04.11 - [Experiment/Unity] - Ramda
Ramda
//선언 public class GameEvent { } public static void testRamda1(string message) => Debug.Log(message); public static void testRamda2() => Debug.Log("testRamda2"); //제너릭 public static void testRamda3(T evt) where T : GameEvent { Debug.Log("testRamde
roaring-stretching-thought-wood.tistory.com
Game / Event.cs
Game / Managers / EventManger.cs와 엮어서 볼 것
아무런 내용이 없는 클래스이더라도 그 이름과 존재 자체만으로 형상화 할 수 있음을 유의
namespace Unity.FPS.Game
{
// The Game Events used across the Game.
// Anytime there is a need for a new event, it should be added here.
public static class Events
{
public static ObjectiveUpdateEvent ObjectiveUpdateEvent = new ObjectiveUpdateEvent();
public static AllObjectivesCompletedEvent AllObjectivesCompletedEvent = new AllObjectivesCompletedEvent();
public static GameOverEvent GameOverEvent = new GameOverEvent();
public static PlayerDeathEvent PlayerDeathEvent = new PlayerDeathEvent();
public static EnemyKillEvent EnemyKillEvent = new EnemyKillEvent();
public static PickupEvent PickupEvent = new PickupEvent();
public static AmmoPickupEvent AmmoPickupEvent = new AmmoPickupEvent();
public static DamageEvent DamageEvent = new DamageEvent();
public static DisplayMessageEvent DisplayMessageEvent = new DisplayMessageEvent();
}
public class ObjectiveUpdateEvent : GameEvent
{
public Objective Objective;
public string DescriptionText;
public string CounterText;
public bool IsComplete;
public string NotificationText;
}
public class AllObjectivesCompletedEvent : GameEvent { }
public class GameOverEvent : GameEvent
{
public bool Win;
}
public class PlayerDeathEvent : GameEvent { }
public class EnemyKillEvent : GameEvent
{
public GameObject Enemy;
public int RemainingEnemyCount;
}
public class PickupEvent : GameEvent
{
public GameObject Pickup;
}
public class AmmoPickupEvent : GameEvent
{
public WeaponController Weapon;
}
public class DamageEvent : GameEvent
{
public GameObject Sender;
public float DamageValue;
}
public class DisplayMessageEvent : GameEvent
{
public string Message;
public float DelayBeforeDisplay;
}
}
class Events
- Event는 메서드에 전달되는 인수이기도 하며, 메서드의 태그처럼 보이기도한다.
- 메서드가 해당하는 이벤트 타입를 인수로 가지면 해당 이벤트를 추적하는 메서드임을 직관적으로 알 수있다.
- 각 이벤트 종류 하나하나가 마치 채널과 같은 역할을 하고 있다고 보여진다.
- 클래스의 멤버나 구성 요소가 아닌, 종류 그 자체가 중요하다.
- EventManager의 Broadcast(GameEvent evt) 메서드에서 도 이벤트 클래스의 타입으로 어느 채널에 방송할 건지 정한다.
- 더불어서 여러 Event의 모양을 여기서 정의하여 한 파일내에서 관리하고 있다.
- 각 이벤트 클래스이 객체를 하나 씩 가진다.
- 브로드 캐스팅하는 클래스는 메세지를 적당히 다뤄야한다.
class ~~ : GameEvent
- GameEvent는 EventManager.cs 에 정의된 Event의 틀
사용 예시
using UnityEngine;
using UnityEngine.SceneManagement;
//GameFlowManager.cs
namespace Unity.FPS.Game
{
public class GameFlowManager : MonoBehaviour
{
//~~~
void EndGame(bool win)
{
//~~~
DisplayMessageEvent displayMessage = Events.DisplayMessageEvent;
displayMessage.Message = WinGameMessage;
displayMessage.DelayBeforeDisplay = DelayBeforeWinMessage;
EventManager.Broadcast(displayMessage);
//~~~
}
}
}
Events 클래스에 모여있는 정적 파생 Event타입의 객체를 가져와서 메세지를 세팅한 뒤 전달한다.
※EventSystem
※ scene 에 포함되어 있는 EventSystem 객체에는 EventSystem 와 StandaloneInputModule 컴포넌트가 있다.
오브젝트의 스크립트
Unity UI의 패키지에 포함되어 있다.
대략적으로 살펴보면 마우스 input과 관련되 있지 않을까 추측된다.
Game / Managers / GameFlowManager.cs
Scene 내 GameManager는 GameFlowManager, EnemyManager, ActorsManager, AudioManager 를 가지고 있다.
namespace Unity.FPS.Game
{
public class GameFlowManager : MonoBehaviour
{
public float EndSceneLoadDelay = 3f;
public CanvasGroup EndGameFadeCanvasGroup;
public string WinSceneName = "WinScene";
public float DelayBeforeFadeToBlack = 4f;
public string WinGameMessage;
public float DelayBeforeWinMessage = 2f;
public AudioClip VictorySound;
public string LoseSceneName = "LoseScene";
public bool GameIsEnding { get; private set; }
float m_TimeLoadEndGameScene;
string m_SceneToLoad;
void Awake()
void Start()
void Update()
void OnAllObjectivesCompleted(AllObjectivesCompletedEvent evt) => EndGame(true);
void OnPlayerDeath(PlayerDeathEvent evt) => EndGame(false);
void EndGame(bool win)
void OnDestroy()
}
}
//활용 클래스, 메서드
public CanvasGroup EndGameFadeCanvasGroup;
//씬 교체하기, m_SceneToLoad은 string으로 해당 이름의 scene 에셋이 존재해야한다.
SceneManager.LoadScene(m_SceneToLoad);
//UI 서서히 드러나게 하기 (with update)
EndGameFadeCanvasGroup.alpha = timeRatio;
//마우스 커서 잠그기
Cursor.lockState = CursorLockMode.None;
//커서 보이게 하기
Cursor.visible = true;
//UI 띄우기
EndGameFadeCanvasGroup.gameObject.SetActive(true);
//시간 추적을 위해 Time.time을 더한다
TimeLoadEndGameScene = Time.time + EndSceneLoadDelay + DelayBeforeFadeToBlack;
//컴포넌트 추가하기 및 예약 사운드 재생
//AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.HUDVictory)은 MixerGroup를 반환하는 메서드 별도 구현 필요
var audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = VictorySound;
audioSource.playOnAwake = false;
audioSource.outputAudioMixerGroup = AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.HUDVictory);
audioSource.PlayScheduled(AudioSettings.dspTime + DelayBeforeWinMessage);
※유니티 에디터에 파라미터 정보 띄우기
//~~~
[Header("Parameters")]
[Tooltip("Duration of the fade-to-black at the end of the game")]
public float EndSceneLoadDelay = 3f;
[Tooltip("The canvas group of the fade-to-black screen")]
public CanvasGroup EndGameFadeCanvasGroup;
[Header("Win")]
[Tooltip("This string has to be the name of the scene you want to load when winning")]
public string WinSceneName = "WinScene";
[Tooltip("Duration of delay before the fade-to-black, if winning")]
public float DelayBeforeFadeToBlack = 4f;
//~~~

class GameFlowManager
- 우선적으로, EndGame()이 호출되는 메서드는 매니져 내부의 OnAllObjectivesCompleted(AllObjectivesCompletedEvent evt), OnPlayerDeath(PlayerDeathEvent evt) 메서드 외에는 없다.
- 외부 코드에서 게임 종료를 직접 참조해서 구현한 것이 아니라. 외부와의 결합을 EventManager 구독으로 최소화 했다.
- 게임 오버 및 승리를 전반적으로 다루고 있다.
- 눈에 띄는 결합은 AudioUtility를 직접 참조한다는 것이다. 오디오 기능이라 정적으로 사용하는 것이다.
- 게임 승리 및 종료 시, 화면이 전환되는 타이밍을 다룬다.
- 설정된 EndSceneLoadDelay, DelayBeforeFadeToBlack,Time.time를 사용해 경우에 따라 적용할 시간 m_TimeLoadEndGameScene를 다르게 계산한다.
- 설정된 WinSceneName, LoseSceneName를 사용해 경우에 따라 적용할 씬이름 m_SceneToLoad를 다르게 사용한다.
- 어디까지나 게임 엔진에서 값을 설정하기 때문에, 설정에 이상하지 않은 값을 사용하고, 그 값을 사용해 내부 값을 선택적으로 적용하는 것을 고려한다.
void Awake(), void Start()
- EventManager는 일반 정적 클래스 이므로 전반적으로 존재하고 GameFlowManager은 MonoBehaviour로 게임객체가 생성될 때 존재한다. 참조 타이밍에 걱정이 없다.
- 정적 요소의 초기 설정을 한다.
※ AudioUtility AudioManager AudioMixer(Unity engine)
void Awake()
{
EventManager.AddListener<AllObjectivesCompletedEvent>(OnAllObjectivesCompleted);
EventManager.AddListener<PlayerDeathEvent>(OnPlayerDeath);
}
void Start()
{
AudioUtility.SetMasterVolume(1);
}
void OnAllObjectivesCompleted(AllObjectivesCompletedEvent evt),void OnPlayerDeath(PlayerDeathEvent evt)
- 세부 구현은 endgame를 하고, 간단히 인수를 조절하는 것으로 구현 => EndGame(true), => EndGame(false)
- 이를 이벤트에 구독시켰다.
- 이런식으로 자체 메서드 구현이 없더라도 메서드 존재, 이름 만으로 형상화 할 수 있다는 것을 알아두자
void Update()
- 승리 및 오버에 시간 지연을 적용하였으므로, 타임을 추적해야 하기 때문에 update() 메서드에 작성되었다.
- 외부에서 정한 계산된 플래그와 실행할 시간이 계산된 값을 사용한다.
void EndGame(bool win)
{
//~~~
GameIsEnding = true;
//~~~
m_TimeLoadEndGameScene = Time.time + ~~~;
//~~~
}
void Update()
{
if (GameIsEnding)
{
float timeRatio = 1 - (m_TimeLoadEndGameScene - Time.time) / EndSceneLoadDelay;
//~~~ 비율을 사용한 UI 투명도, 소리 서서히 드러난다.
if (Time.time >= m_TimeLoadEndGameScene)
{
//~~~ 충분히 지연된 다음 실행
}
}
}
- 타임을 다루기 위해서는 게임 시간을 기준으로 추적한다. 따라서 Time.time 값을 기준으로 하기 때문에 이를 더해서 사용한다.
void EndGame(bool win)
- 몇가지 설정된 값을 사용하여, 경우에 따라 실제로 적용할 float m_TimeLoadEndGameScene, string m_SceneToLoad 를 설정한다.
- update 문에 활성화 시킬 플래그를 성정한다.
Game / Managers / ActorsManager.cs
Scene 내 GameManager의 컴포넌트
namespace Unity.FPS.Game
{
public class ActorsManager : MonoBehaviour
{
public List<Actor> Actors { get; private set; }
public GameObject Player { get; private set; }
public void SetPlayer(GameObject player) => Player = player;
void Awake()
{
Actors = new List<Actor>();
}
}
}
Actors는 List<T> 이고 대부분의 리스트 관련 메서드를 사용가능하다.
//~Actor.cs
//활용
m_ActorsManager = GameObject.FindFirstObjectByType<ActorsManager>();
m_ActorsManager.Actors.Contains(this)
m_ActorsManager.Actors.Add(this)
m_ActorsManager.Actors.Remove(this);
간단한 배열이지만, 이 배열에 엑터를 등록시켜서 사용함.
여러 액터에 쉽게 접근 할 수 있게 제공함.
외부 모듈에서 플레이어 객체를 직접 참조하기 보다는, 매니져에 등록된 플레이어를 참조하게 함으로써, 어딘가에 있을 플레이어 클래스와 외부 모듈의 결합을 방지하기 위함인것 같다.
오브젝트 수준의 서비스 어로케이터 정도라 이해한다면 될 것같다.
Game / Actor.cs
모든 액터가 가져야 하는 컴포넌트
주석 내용에, 액터가 가지는 일반적인 정보, AI 감지 로직과 팀 여부를 판단하는데 사용된다 한다.
namespace Unity.FPS.Game
{
public class Actor : MonoBehaviour
{
public int Affiliation; // 소속 팀
public Transform AimPoint; // 상대방의 조준점
ActorsManager m_ActorsManager;
void Start()
void OnDestroy()
}
}
//활용
m_ActorsManager = GameObject.FindFirstObjectByType<ActorsManager>();
FPS 게임 속 액터가 필요한 기본적인 데이터를 가지고 있다.
이런 류의 멤버는 설계단계에서 고려해야 할까?
class Actor
- 여기서는 적 AI 로직에 대해 필요한 변수를 멤버로 가짐
- 자신을 등록할 매니져
void Start()
- 매니져를 찾고 자신을 매니져에 등록함
void OnDestroy()
- 매니져에서 등록 해제
Game / Managers / AudioManager
Scene 내의 GameManager의 컴포넌트
유니티 에디터에서 에셋에 audio mixer 와 함께 볼것
#오디오 엔진의 내부 동작은 자세히 모르므로 대강의 사용 방법만 알아보자
namespace Unity.FPS.Game
{
public class AudioManager : MonoBehaviour
{
public AudioMixer[] AudioMixers;
public AudioMixerGroup[] FindMatchingGroups(string subPath)
public void SetFloat(string name, float value)
public void GetFloat(string name, out float value)
}
}
//활용
//유니티 엔진의 오디오 클래스 AudioMixer
public AudioMixer[] AudioMixers;
//유니티 엔진의 오디오 클래스 AudioMixerGroup, AudioMixer.FindMatchingGroups(string)
AudioMixerGroup[] results = AudioMixers[i].FindMatchingGroups(subPath);
//AudioMixer.SetFloat(string, float)
AudioMixers[i].SetFloat(name, value);
//AudioMixer.GetFloat(string, out float)
AudioMixers[i].GetFloat(name, out value);
※ 유니티 에디터를 통해 유니티 에셋에서 Audio Mixer를 만들수 있다. 그 Audio Mixer에서 Groups를 만들 수 있다.

class AudioManager
- AudioMixer 배열을 가짐으로써 에디터에서 에셋에 만든 Audio Mixer를 등록 가능하다.
- 각 그룹은 계층적이다. 마스터를 조절하면 그 하위 계층도 영향 받는다.
- 이때 string을 통해 그룹의 개체에 접근가능하다.
- AudioMixer는 하나지만, 여러개를 사용할 수 있도록 AudioMixer[]를 사용했다.
- AudioManager의 주 역할은 Audio Mixer가 여러개 일때를 다루기 위한 클래스이다.
AudioMixerGroup[] FindMatchingGroups(string subPath)
- AudioMixer는 계층적인 그룹을 가진다.(사진처럼) 그룹의 명을 이용해 string 조회하고으로 반환한다.
void SetFloat(string name, float value)
- 해당 name의 그룹의 데시벨 값을 value로 바꾼다.
- AudioMixers.SetFloat(name, value) 메서드는 return 타입이 bool 타입으로, 아마 실패하면 에러가 아니라 bool값을 리턴할 것이다. 따라서 실패를 상관하지 않는다.
public void SetFloat(string name, float value)
{
for (int i = 0; i < AudioMixers.Length; i++)
{
if (AudioMixers[i] != null)
{
AudioMixers[i].SetFloat(name, value);
}
}
}
void GetFloat(string name, out float value)
- 딱히 볼륨값을 가져오는건 구현만 되어 있지 사용은 안하듯 하다. 참조는 한군데 되어있지만, 그것의 참조는 없었다.
public void GetFloat(string name, out float value)
{
value = 0f;
for (int i = 0; i < AudioMixers.Length; i++)
{
if (AudioMixers[i] != null)
{
AudioMixers[i].GetFloat(name, out value);
break;
}
}
}
Game / AudioUtility.cs
실제로 소리가 재생되는 로직이 담김,
임시 오브젝트를 생성해 소리를 재생하고 반환시키는데, 이때 소리가 끝날때 반환하도록하는 방법에 유의
namespace Unity.FPS.Game
{
public class AudioUtility
{
static AudioManager s_AudioManager;
public enum AudioGroups
{
DamageTick,
Impact,
EnemyDetection,
Pickup,
WeaponShoot,
WeaponOverheat,
WeaponChargeBuildup,
WeaponChargeLoop,
HUDVictory,
HUDObjective,
EnemyAttack
}
public static void CreateSFX(AudioClip clip, Vector3 position, AudioGroups audioGroup, float spatialBlend,
float rolloffDistanceMin = 1f)
public static AudioMixerGroup GetAudioGroup(AudioGroups group)
public static void SetMasterVolume(float value)
public static float GetMasterVolume()
}
}
//활용
//enum을 손쉽게 문자열값으로
public static AudioMixerGroup GetAudioGroup(AudioGroups group)
{ //~~~
group.ToString()
//~~~
}
Object.FindFirstObjectByType<AudioManager>();
//데시벨 변환
float valueInDb = Mathf.Log10(value) * 20;
//역변환
Mathf.Pow(10f, valueInDb / 20.0f);
//유의한 방법
//임시 생성한 오브젝트가 오디오가 끝나면 사라지도록 구현된 컴포넌트를 장착함.
GameObject impactSfxInstance = new GameObject();
impactSfxInstance.transform.position = position;
source.clip = clip;
source.Play();
source.outputAudioMixerGroup = GetAudioGroup(audioGroup);
TimedSelfDestruct timedSelfDestruct = impactSfxInstance.AddComponent<TimedSelfDestruct>();
timedSelfDestruct.LifeTime = clip.length;

class AudioUtility
- enum을 통해, 몇가지 Mixer group를 작성해 놓았다.
- 단순한 int형 취급이라 실제로 문자열로 매칭할 때 이에 대한 고려가 필요하다.
void CreateSFX(AudioClip clip, Vector3 position, AudioGroups audioGroup, float spatialBlend,
float rolloffDistanceMin = 1f)
- 새로운 소리를 재생 할때, 임시 오브젝트를 생성해서 position을 적용한 뒤 .AddComponent<AudioSource>();
- 반환된 AudioSource를 에 바로 설정값을 적용, 내부 메서드 GetAudioGroup(audioGroup)를 사용해 믹서그룹 매칭
- 임시 생성된 오브젝트는 별도 구현된 TimedSelfDestruct 컴포넌트를 붙여, 시간이 되면 사라지도록 만듬
public static void CreateSFX(AudioClip clip, Vector3 position, AudioGroups audioGroup, float spatialBlend,
float rolloffDistanceMin = 1f)
{
//임시 오브젝트 생성 및 소리 재생 컴포넌트 추가
GameObject impactSfxInstance = new GameObject();
impactSfxInstance.transform.position = position;
AudioSource source = impactSfxInstance.AddComponent<AudioSource>();
//소리 재생 컴포넌트 설정 및 재생
source.clip = clip;
source.spatialBlend = spatialBlend;
source.minDistance = rolloffDistanceMin;
source.Play();
source.outputAudioMixerGroup = GetAudioGroup(audioGroup);
//소리가 끝나면 오브젝트 반환하도록 컴포넌트 구현해서 사용
TimedSelfDestruct timedSelfDestruct = impactSfxInstance.AddComponent<TimedSelfDestruct>();
timedSelfDestruct.LifeTime = clip.length;
}
AudioUtility.CreateSFX(OnDetectSfx, transform.position, AudioUtility.AudioGroups.EnemyDetection, 1f);
AudioMixerGroup GetAudioGroup(AudioGroups group)
- enum 에 맞춘 믹서 그룹을 반환해줌
- enum이 int 취급이기 때문에, 문자열로 찻아야하는 믹서그룹을 위해, group.ToString()으로 열거형을 손쉽게 문자열로 전환해서 검색
void SetMasterVolume(float value), float GetMasterVolume()
- 상대적인 크기와 데시벨 값을 변환 시켜서 마스터 볼륨 설정 및 반환
Game / TimedSelfDestruct.cs
임시 오브젝트 시간에 따라 파괴하기
namespace Unity.FPS.Game
{
public class TimedSelfDestruct : MonoBehaviour
{
public float LifeTime = 1f;
float m_SpawnTime;
void Awake()
{
m_SpawnTime = Time.time;
}
void Update()
{
if (Time.time > m_SpawnTime + LifeTime)
{
Destroy(gameObject);
}
}
}
}
임시로 생성한 오브젝트를 자동적으로 반환 시킬때, 이 컴포넌트를 .AddComponent<TimedSelfDestruct>();
LifeTime 설정.
오브젝트 추적없이 설정한 값이 되면 반환
Game / Managers / ObjectiveManager
목적 매니저.
namespace Unity.FPS.Game
{
public class ObjectiveManager : MonoBehaviour
{
List<Objective> m_Objectives = new List<Objective>();
bool m_ObjectivesCompleted = false;
void Awake()
void RegisterObjective(Objective objective) => m_Objectives.Add(objective);
void Update()
void OnDestroy()
}
}
class OjectiveManager
- Objective에서 선언한 event를 registerObjective가 구독한다.
- 일단 Objective는 목표 클래스의 최소한의 추상화인것 같다. 이때 굳이 Objective에서 스스로를 매니져에 등록하여 결합고를 만들기 보단, 분리를 유지할 수 있는 방법을 적용하는게 맞다. 이렇게 Objective는 매니져와 덜 결합된 상태를 유지한다.
Awake()
//~Objective.cs
namespace Unity.FPS.Game
{
public abstract class Objective : MonoBehaviour
{
//~~~
public static event Action<Objective> OnObjectiveCreated;
//~~
}
}
namespace Unity.FPS.Game
{
public class ObjectiveManager : MonoBehaviour
{
//~~~
void Awake()
{
Objective.OnObjectiveCreated += RegisterObjective;
}
//~~~
}
}
- Objective가 선언한 이벤트를 구독한다. event 를 변수로 생성하는 데, delegate 모양만 잡아논 변수인거 같다.
void RegisterObjective(Objective objective) => m_Objectives.Add(objective);
//~Objective.cs
OnObjectiveCreated?.Invoke(this);
//~ObjectiveManager.cs
void RegisterObjective(Objective objective) => m_Objectives.Add(objective);
- nObjectiveCreated event를 구독했기 때문에, 자연스럽게 리스트에 객체를 등록시킨다.
- 단순히 Add를 구독시킬게 아니라 메서드 명을 자체만으로 형상화 했다고 생각한다.
void Update()
//~Objective
public bool IsBlocking() => !(IsOptional || IsCompleted);
//IsOptional은 승리조건 포함여부, IsCompleted는 완료된 목적, IsOptional은 완료 안해도 되는 목적
- 등록된 목적이 1 이상이고, 목적의 IsBlocking()이 false면
- m_ObjectivesCompleted = true;
- EventManager.Broadcast(Events.AllObjectivesCompletedEvent);
void OnDestroy()
- 구독해제
Game / Shared / Objective.cs
달성 목표를 나타낸다. 여러 파생 달성 목표가 상속하였다.
namespace Unity.FPS.Game
{
public abstract class Objective : MonoBehaviour
{
public string Title;
public string Description;
public bool IsOptional;
public float DelayVisible;
public bool IsCompleted { get; private set; }
public bool IsBlocking() => !(IsOptional || IsCompleted);
public static event Action<Objective> OnObjectiveCreated;
public static event Action<Objective> OnObjectiveCompleted;
protected virtual void Start()
public void UpdateObjective(string descriptionText, string counterText, string notificationText)
public void CompleteObjective(string descriptionText, string counterText, string notificationText)
}
}
class Objective : MonoBehaviour
- 달성 목표를 게이머에게 이해시키는데 필요한 제목, 설명과 오브젝트가 부가 목표설정가능하다.
- IsCompleted와 IsOptional을 사용해 IsBlocking() => !(IsOptional || IsCompleted);를 작성하여, 목표를 추적할때 선택적으로 추적할 필요없이 전체다 추적하면 된다.
- 매니져를 호출해 직접 등록하기 보다는, 이벤트를 작성하고 매니져가 자신을 추적하게 하는게, 덜 결합도 있다. 물론 누락되지 않도록 신경은 써야할 것이다.
- 일단은 게임이벤트매니져가 존재하지만 별도로 이벤트를 설정하는 것은 발송지가 제한적이기 때문인 것 같다.
public bool IsBlocking() => !(IsOptional || IsCompleted);
- 선택 옵션을 고려하려 조회하려면, IsOptional, IsCompleted 둘 모두 참고해야 할것이다.
- 차라리 IsBlocking() 를 통해 제공하면, 추상화도 좋고 캡슐도 좋다.
virtual void Start()
protected virtual void Start()
{
OnObjectiveCreated?.Invoke(this);
//커스텀 클래스
DisplayMessageEvent displayMessage = Events.DisplayMessageEvent;
//~~~ 이벤트 객체 세팅
EventManager.Broadcast(displayMessage);
}
- 파생 클래스를 고려하여 virtual 이 선언되었다.
- 이벤트를 발생시켜 매니져가 추적하여 등록하도록한다.
- 화면을 띄우기 위한 게임 이벤트를 이벤트 매니져를 통해 전달한다.
public void UpdateObjective(string descriptionText, string counterText, string notificationText),
public void CompleteObjective(string descriptionText, string counterText, string notificationText)
- 달성 목표 텍스트를 변경해 전달한다.
- Complete의 경우 플래그 값을 바꾸어 전달한다.
이벤트 매니져로 게임을 구성하면 정말 잘 적어놔야겠다. 어떤 이벤트는 참조가 많다.
Gameplay / Objectives / ObjectiveKillEnemies.cs
달성 목표를 구체화한 클래스, 적 처치
namespace Unity.FPS.Gameplay
{
public class ObjectiveKillEnemies : Objective
{
public bool MustKillAllEnemies = true;
public int KillsToCompleteObjective = 5;
public int NotificationEnemiesRemainingThreshold = 3;
int m_KillTotal;
protected override void Start()
void OnEnemyKilled(EnemyKillEvent evt)
string GetUpdatedCounterAmount()
void OnDestroy()
}
}
//활용
//virtual, override라 할지라도 base.Start();를 통해 부모 메서드 호출
base.Start();
//string과 + 연산자, bool ? true : false
Title = "Eliminate " + (MustKillAllEnemies ? "all the" : KillsToCompleteObjective.ToString()) + " enemies";
//string 값 확인
string.IsNullOrEmpty(Title)
class ObjectiveKillEnemies : Objective
- Objective를 상속 받아 구현된 달성목표
- 적 처지에 대한 달성목표로 세부 설정 가능한 에디터용 변수와 킬수를 추적하는 변수가 잇다.
override void Start()
- base.Start()를 통해 기본적으로 실행되야하는 과정을 실행한다.
- base는 자신이 가진 것들로 할 수 있는 작업을, 자식은 자식이 가진 변수로 추가적으로 해야하는 작업을 한다.
- 중복 되지 않도록 조심해야 할것이다.
- 의아한건 베이스에서 메세지에 타이틀을 전달하고 뒤늦게 타이틀이 없을 경우을 확인하여 타이틀을 설정한다는 것이다.
- 굳이 순서를 안바꿀 이유는 없어보인다. 혹은 사용시 Title이 없는 경우 자체를 어느정도 배제하기 위해서일 수도?
- EventManager.AddListener<EnemyKillEvent>(OnEnemyKilled);를 통해 적 사망을 구독했다.
void OnEnemyKilled(EnemyKillEvent evt)
- 처지된 적 수를 추적하고 objective 메서드를 사용해 ObjectiveUpdateEvent 를 전송한다.
string GetUpdatedCounterAmount()
- 달성도를 표시해 주기 위한 string 반환 메서드
void OnDestroy()
- 구독 해제
Gameplay / Objectives / ObjectivePickupItem.cs
달성 목표를 구체화한 클래스, 아이템 수집
namespace Unity.FPS.Gameplay
{
public class ObjectivePickupItem : Objective
{
public GameObject ItemToPickup;
protected override void Start()
void OnPickupEvent(PickupEvent evt)
void OnDestroy()
}
}
class ObjectivePickupItem : Objective
- 구성이 단출한데, 수집해야할 Pickup 아이템이 하나 일때를 구현한 것이다.
- 에디터에서 오브젝트를 끌어다 적용할 수 있도록 하였다.
- 어디까지나 에디터에서 유용하게 사용하기 위한 코드임을 생각한다.
override void Start()
- base.Start()에서 대부분의 셋업이 끝나고, 이벤트를 구독한다.EventManager.AddListener<PickupEvent>(OnPickupEvent)
- 타이틀하고 세부사항은 어느정도 에디터에서 입력해준다는 전제가 있는것 같다.
void OnPickupEvent(PickupEvent evt)
- 플래그가 언제 설정되는 지도 잘 기억해 놓는다.
- 플래그를 사용한 if 문이 있는데 하나는 동작 완료 후 스킵이고 하나는 조건 미충족 스킵이다.
void OnDestroy()
- 구독 해제
Gameplay / Objectives / ObjectiveReachPoint.cs
달성 목표의 구체화. 위치 도착 포인트
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(Collider))]
public class ObjectiveReachPoint : Objective
{
public Transform DestroyRoot;
void Awake()
void OnTriggerEnter(Collider other)
}
}
//활용
//에디터, 필수 컴포넌트
[RequireComponent(typeof(Collider))]
//var 및 .GetComponent<T>()
var player = other.GetComponent<PlayerCharacterController>();
//MonoBehavior 멤버
transform
//OnTriggerEnter 메서드
void OnTriggerEnter(Collider other)
class ObjectiveReachPoint : Objective
- Objective는 MonoBehaviour의 파생형이므로 transform를 멤버로 가진다는 것을 알아두자.
- 위치를 나타내는 멤버를 가진다. 이때, MonoBehaviour도 위치를 이미 가지고 있다는 것도 알아두자
void Awake()
- 설정이 안돼어 있다면, 게임 오브젝트이 위치로 설정
void OnTriggerEnter(Collider other)
- 이건 콜리더가 트리거 되면 자동으로 호출된다.
- 이해를 위해서는 에디터의 게임 오브젝트가 이 컴포넌트와 콜리더 컴포넌트를 가지고 있는 상황에서 시작해야한다.
- 콜리더는 충돌시에 void OnTriggerEnter(Collider other)를 호출한다.
- 따라서 별도의 구독 없이도 이미 자체 내장된 방식이 존재한다.
Game / Shared / Health.cs
체력 관련
UnityAction<float, GameObject> OnDamaged;
UnityAction<float> OnHealed;
UnityAction OnDie;
위 딜리게이트가 invoke 된다.
OnDie 를 invoke하지만 사망 관련 리액션은 없음
public class Health : MonoBehaviour
{
public float MaxHealth = 10f;
public float CriticalHealthRatio = 0.3f;
public UnityAction<float, GameObject> OnDamaged;
public UnityAction<float> OnHealed;
public UnityAction OnDie;
public float CurrentHealth { get; set; }
public bool Invincible { get; set; }
public bool CanPickup() => CurrentHealth < MaxHealth;
public float GetRatio() => CurrentHealth / MaxHealth;
public bool IsCritical() => GetRatio() <= CriticalHealthRatio;
bool m_IsDead;
void Start()
public void Heal(float healAmount)
public void TakeDamage(float damage, GameObject damageSource)
public void Kill()
void HandleDeath()
}
//활용
//범위내 값을 리턴, 0~ MaxHealth범위내에서 CurrentHealth 값을 반환
CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
//딜리게이트
public UnityAction<float> OnHealed;
OnDamaged?.Invoke(trueDamageAmount, damageSource);
//조회용 함수 작성
public bool CanPickup() => CurrentHealth < MaxHealth;
public float GetRatio() => CurrentHealth / MaxHealth;
public bool IsCritical() => GetRatio() <= CriticalHealthRatio;
//Handle 명명
HandleDeath()
//상태 알림
public UnityAction<float, GameObject> OnDamaged;
public UnityAction<float> OnHealed;
public UnityAction OnDie;
class Health : MonoBehaviour
- 체력 관련 컴포넌트이다.
- invoke를 위한 델리게이트 세개를 가지고 있다. 각 각은 데미지를 적용시키기 보다 애니메이션 혹은 UI를 위한 것 같다.
- 체력량 혹은 무적여부를 추적하는 멤버
- 간단한 상태, 체력이 가득한지, 몇 퍼센트 남았는지, 위중하지 확인할 수 있는 매서드를 가진다.
bool CanPickup() => CurrentHealth < MaxHealth;
float GetRatio() => CurrentHealth / MaxHealth;
bool IsCritical() => GetRatio() <= CriticalHealthRatio;
- 외부에서 조회하기 위한 메서드이다.
- 직접 체력량을 전달하기 보단, 필요한 정보만 전달한다.
void Start()
- 추적하는 체력을 최대체력으로 초기화한다.
void Heal(float healAmount)
- 체력 추적값에 힐을 적용하고, 최대 생명력을 넘으면 조정한다.
- 실제 힐량을 계산해 Onheal 딜리게이트에 값을 전달하면서 invoke 한다.
void TakeDamage(float damage, GameObject damageSource)
- 데미지를 적용하고 실제 입은 데미지를 계산해 invoke 한다.
void Kill()
- 죽을 경우가 아니라 죽이기 위한 메서드이다.
HandleDeath()
- 체력 값을 확인하고 죽은 상황이 맞다면 OnDie를 invoke한다.
Game / Shared / Damageable.cs
Health에 데미지가 구현되어있는데 왜 필요했을까?
데미지 유형에 관한 내용
namespace Unity.FPS.Game
{
public class Damageable : MonoBehaviour
{
public float DamageMultiplier = 1f;
[Range(0, 1)]
public float SensibilityToSelfdamage = 0.5f;
public Health Health { get; private set; }
void Awake()
public void InflictDamage(float damage, bool isExplosionDamage, GameObject damageSource)
}
}
//활용
Health = GetComponent<Health>();
Health = GetComponentInParent<Health>();
//에디터
[Range(0, 1)]
class Damageable : MonoBehaviour
- 데미지 배율에 관한 변수를 가진다.
void Awake()
- Health 컴포넌트를 찾아 추적
void InflictDamage(float damage, bool isExplosionDamage, GameObject damageSource)
- 폭발형 데미지 인지 아닌지, 본인이 유발한 건지 아닌지 확인하고 배율을 적용한다.
Game / Shared / Destructable.cs
파괴 가능한 물체, 체력이 0일때 리액션이 구현됨
namespace Unity.FPS.Game
{
public class Destructable : MonoBehaviour
{
Health m_Health;
void Start()
void OnDamaged(float damage, GameObject damageSource)
void OnDie()
}
}
//활용
//외부 딜리게이트 사용 명명
m_Health.OnDie += OnDie;
//마치 외부에서 메서드 명을 선언하고 여기서 구현한 느낌이다.
class Destructable : MonoBehaviour
- 단순하다. 솔직히 별 동작을 한다고 보기 힘들다. Destroy(gameObject); 를 동작시킬 뿐이다.
- 그럼에도 불구하고 체력과 사망리액션을 분리함으로써, 체력 상황과 동작이 분리되었다.
void Start()
- Health 컴포넌트 구현 및 필요 딜리게이트 구독
void OnDamaged(float damage, GameObject damageSource)
- 별다른 구현내용은 없지만 추가 이펙트를 구현할 수 있게 공간만 만들어둔듯 하다.
void OnDie()
- 객체 반환
Game / Shared / WeaponController.cs
총 프리펩에 붙는 컴포넌트
public UnityAction OnShoot;
namespace Unity.FPS.Game
{
public enum WeaponShootType
{
Manual,
Automatic,
Charge,
}
public struct CrosshairData
{
public Sprite CrosshairSprite;
public int CrosshairSize;
public Color CrosshairColor;
}
public class WeaponController : MonoBehaviour
{
[Header("Information")]
//웨폰 기본정보, 조준점
public string WeaponName;
public Sprite WeaponIcon;
public CrosshairData CrosshairDataDefault;
public CrosshairData CrosshairDataTargetInSight;
[Header("Internal References")]
//웨폰 프리펩 관련, 총 중앙점, 발사구
public GameObject WeaponRoot;
public Transform WeaponMuzzle;
[Header("Shoot Parameters")]
//위에 열거형 대로 에디터에서 설정 가능
public WeaponShootType ShootType;
//투사체프리팹
public ProjectileBase ProjectilePrefab;
//발사 관련
public float DelayBetweenShots = 0.5f;
public float BulletSpreadAngle = 0f;
public int BulletsPerShot = 1;
[Range(0f, 2f)]
public float RecoilForce = 1;
[Range(0f, 1f)]
public float AimZoomRatio = 1f;
public Vector3 AimOffset;
[Header("Ammo Parameters")]
//재장전 관련
public bool AutomaticReload = true;
public bool HasPhysicalBullets = false;
public int ClipSize = 30;
public GameObject ShellCasing;
public Transform EjectionPort;
[Range(0.0f, 5.0f)]
public float ShellCasingEjectionForce = 2.0f;
[Range(1, 30)]
public int ShellPoolSize = 1;
public float AmmoReloadRate = 1f;
public float AmmoReloadDelay = 2f;
public int MaxAmmo = 8;
[Header("Charging parameters (charging weapons only)")]
//차지샷
public bool AutomaticReleaseOnCharged;
public float MaxChargeDuration = 2f;
public float AmmoUsedOnStartCharge = 1f;
public float AmmoUsageRateWhileCharging = 1f;
[Header("Audio & Visual")]
//효과
public Animator WeaponAnimator;
public GameObject MuzzleFlashPrefab;
public bool UnparentMuzzleFlash;
public AudioClip ShootSfx;
public AudioClip ChangeWeaponSfx;
[Tooltip("Continuous Shooting Sound")]
//부가 음향효과
public bool UseContinuousShootSound = false;
public AudioClip ContinuousShootStartSfx;
public AudioClip ContinuousShootLoopSfx;
public AudioClip ContinuousShootEndSfx;
AudioSource m_ContinuousShootAudioSource = null;
bool m_WantsToShoot = false;
//하나는 왜 유니티 액션일까.
public UnityAction OnShoot;
public event Action OnShootProcessed;
//추적용
int m_CarriedPhysicalBullets;
float m_CurrentAmmo;
float m_LastTimeShot = Mathf.NegativeInfinity;
public float LastChargeTriggerTimestamp { get; private set; }
Vector3 m_LastMuzzlePosition;
//내부 변수
public GameObject Owner { get; set; }
public GameObject SourcePrefab { get; set; }
//토글용
public bool IsCharging { get; private set; }
public float CurrentAmmoRatio { get; private set; }
public bool IsWeaponActive { get; private set; }
public bool IsCooling { get; private set; }
public float CurrentCharge { get; private set; }
public Vector3 MuzzleWorldVelocity { get; private set; }
//간단 메서드
public float GetAmmoNeededToShoot() =>
(ShootType != WeaponShootType.Charge ? 1f : Mathf.Max(1f, AmmoUsedOnStartCharge)) /
(MaxAmmo * BulletsPerShot);
public int GetCarriedPhysicalBullets() => m_CarriedPhysicalBullets;
public int GetCurrentAmmo() => Mathf.FloorToInt(m_CurrentAmmo);
//저장용
AudioSource m_ShootAudioSource;
//조회용
public bool IsReloading { get; private set; }
//애니메이션 재생용 키워드
const string k_AnimAttackParameter = "Attack";
//탄피 오브젝트 풀(투사체 아님)
private Queue<Rigidbody> m_PhysicalAmmoPool;
void Awake()
public void AddCarriablePhysicalBullets(int count) => m_CarriedPhysicalBullets = Mathf.Max(m_CarriedPhysicalBullets + count, MaxAmmo);
void ShootShell()
void PlaySFX(AudioClip sfx) => AudioUtility.CreateSFX(sfx, transform.position, AudioUtility.AudioGroups.WeaponShoot, 0.0f);
void Reload()
public void StartReloadAnimation()
void Update()
void UpdateAmmo()
void UpdateCharge()
void UpdateContinuousShootSound()
public void ShowWeapon(bool show)
public void UseAmmo(float amount)
public bool HandleShootInputs(bool inputDown, bool inputHeld, bool inputUp)
bool TryShoot()
bool TryBeginCharge()
bool TryReleaseCharge()
void HandleShoot()
public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
}
//에디터
public enum WeaponShootType
{
Manual,
Automatic,
Charge,
}
// 위 열거형대로 에디터에서 선택가능
public WeaponShootType ShootType;
//활용
//float -> int 가장 가까운 작은 수
Mathf.FloorToInt(m_CurrentAmmo)
//탄약 초기화
Rigidbody nextShell = m_PhysicalAmmoPool.Dequeue();
nextShell.transform.position = EjectionPort.transform.position;
nextShell.transform.rotation = EjectionPort.transform.rotation;
nextShell.gameObject.SetActive(true);
nextShell.transform.SetParent(null);
nextShell.collisionDetectionMode = CollisionDetectionMode.Continuous;
nextShell.AddForce(nextShell.transform.up * ShellCasingEjectionForce, ForceMode.Impulse);
m_PhysicalAmmoPool.Enqueue(nextShell);
//애니메이터 트리거
GetComponent<Animator>().SetTrigger("Reload");
//가장 작은 음수
float m_LastTimeShot = Mathf.NegativeInfinity;
//가장 큰 양수
Mathf.Infinity
//소리재생
m_ShootAudioSource.PlayOneShot(ShootSfx);
//? : 문
int bulletsPerShotFinal = ShootType == WeaponShootType.Charge
? Mathf.CeilToInt(CurrentCharge * BulletsPerShot)
: BulletsPerShot;
//방향에 랜덤 입히기(탄 흩어짐)
Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere,
spreadAngleRatio);
//투사체 생성
ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
Quaternion.LookRotation(shotDirection));
class WeaponController : MonoBehaviour
- 웨펀 프리셋에 붙어 총의 기본적인 동작이 구현되었다.
- 3d 적 데이터는 에디터로 받는다.
- 총이 가지는 모양, 소리, 기능을 가진다.
float GetAmmoNeededToShoot() =>
(ShootType != WeaponShootType.Charge ? 1f : Mathf.Max(1f, AmmoUsedOnStartCharge)) /
(MaxAmmo * BulletsPerShot);
int GetCarriedPhysicalBullets() => m_CarriedPhysicalBullets;
int GetCurrentAmmo() => Mathf.FloorToInt(m_CurrentAmmo);
- Ui에 탄약 관련을 띄우기 위한 조회용 메서드 일것 같다.
- m_CurrentAmmo는 일단 내부에서 float으로 관리된다.
void Awake()
- 추적용 변수 세팅
- 필요 컴포넌트 참조 세팅
- 설정 플래그 당 추가 설정요소 세팅
public void AddCarriablePhysicalBullets(int count) => m_CarriedPhysicalBullets = Mathf.Max(m_CarriedPhysicalBullets + count, MaxAmmo);
- 탄약 추가 획득
- MaxAmmo는 가질 수 있는 최대 탄약
void ShootShell()
- 탄약 발사, 아마도 물리엔진 내에서 탄약이 처리 될듯하다.
void PlaySFX(AudioClip sfx) => AudioUtility.CreateSFX(sfx, transform.position, AudioUtility.AudioGroups.WeaponShoot, 0.0f);
- 코드 작성툴에서 사용이 안되었다고 나와있는데
- 간단히 사운드 재생
void Reload()
- 코드 작성툴에서 사용이 안되었다고 나와있는데
- 리로드 중인지 나타내는 플래그를 조정한다.(false)
public void StartReloadAnimation()
- 리로드 중인지 나타내는 플래그를 조정한다.(true)
- 애니메이터 트리거가 작성됨
void Update()
- 탄약 및 에너지, 소리 관련 호출
- 프레임마다 속도를 계산해 추적한다
if (Time.deltaTime > 0)
{
MuzzleWorldVelocity = (WeaponMuzzle.position - m_LastMuzzlePosition) / Time.deltaTime;
m_LastMuzzlePosition = WeaponMuzzle.position;
}
void UpdateAmmo()
- 재장전에 대한 부분인 것 같다.
- m_CurrentAmmo += AmmoReloadRate * Time.deltaTime; 이므로 m_CurrentAmmo는 float 이여야한다.
- 만약 int값이면 프레임 구간에 따라 손실되는 값이 달랐을 것,
void UpdateCharge()
- 차지 도중에 대한 내용인 것 같다.
void UpdateContinuousShootSound()
- m_ShootAudioSource.PlayOneShot(ShootSfx);
- 믹서를 통하지 않고 소리 재생
void ShowWeapon(bool show)
- 무기 표시 전환
- 무기 전환을 구현하기 위해 필요한 메서드
void UseAmmo(float amount)
- 탄약 수치가 실제로 감소하는 메서드, 물리탄이나 에너지은 각 각 다른 변수로 추적중이었다.
- 차지샷 전용 메서드, 투사체가 나가기 전에 차지 시작 및 도중에 소모되는 량을 표현하기 위함.
bool HandleShootInputs(bool inputDown, bool inputHeld, bool inputUp)
- 총기 타입에 따라 케이스 문을 이용해 적절한 메서드를 맵핑
bool TryShoot()
- 발사 간격 등 발사 가능 여부 확인
bool TryBeginCharge()
- 가능 확인 및 차지 시작
bool TryReleaseCharge()
- 차지 해제 시 발사
void HandleShoot()
- 모든 종류의 총이 결국 이 메서드로 총을 발사한다.
- 발사시 효과도 담당한다.
- OnShoot?.Invoke(); OnShootProcessed?.Invoke();
- 에너지 사용과 발사체 사용 (추적 변수 갱신)이 다르니 주의
Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
- 탄이 흩어져서 발사되기 위한 벡터를 반환
Game / Shared / ProjectileBase.cs
투사체 기본 클래스
이를 상속받는 ProjectileStandard.cs 존재
내 생각에는 투사체가 본격적으로 구현되기 전에, 외부 시스템에서 어떤 정보가 필요한지 정해야해서 간략히 정한 클래스.
public UnityAction OnShoot;
namespace Unity.FPS.Game
{
public abstract class ProjectileBase : MonoBehaviour
{
public GameObject Owner { get; private set; }
public Vector3 InitialPosition { get; private set; }
public Vector3 InitialDirection { get; private set; }
public Vector3 InheritedMuzzleVelocity { get; private set; }
public float InitialCharge { get; private set; }
public UnityAction OnShoot;
public void Shoot(WeaponController controller)
}
}
abstract class ProjectileBase : MonoBehaviour
- 투사체는 풀을 사용하지 않고 Instantiate()으로 생성된다.
- 발사자에서 비롯된 속도를 같이 받는다. InheritedMuzzleVelocity
- 발사자와의 결합이 있다.
- public UnityAction OnShoot; 은 아직 잘 모르겠다
- 이를 상속받는 ProjectileStandard 클래스가 있다.
void Shoot(WeaponController controller)
- 초기 위치와 방향은 어째서 저장해 놓는거지 -> 탄의 1인칭 시야 보정을 위해서
Game / Shared / DamageArea.cs
데미지 받는 스플 범위, 어느 공간을 지정하는 게 아니라, 총알의 폭발범위를 나타내기 위함인것 같다.
투사체에 적용됨
namespace Unity.FPS.Game
{
public class DamageArea : MonoBehaviour
{
public float AreaOfEffectDistance = 5f;
public AnimationCurve DamageRatioOverDistance;
//디버그용
public Color AreaOfEffectColor = Color.red * 0.5f;
public void InflictDamageInArea(float damage, Vector3 center, LayerMask layers,
QueryTriggerInteraction interaction, GameObject owner)
void OnDrawGizmosSelected()
}
}
//활용
//곡선 함수 적용 가능
public AnimationCurve DamageRatioOverDistance;
//구형 범위 나타내기
void OnDrawGizmosSelected()
{
Gizmos.color = AreaOfEffectColor;
Gizmos.DrawSphere(transform.position, AreaOfEffectDistance);
}
//QueryTriggerInteraction
public void InflictDamageInArea(float damage, Vector3 center, LayerMask layers,
QueryTriggerInteraction interaction, GameObject owner)
Collider[] affectedColliders = Physics.OverlapSphere(center, AreaOfEffectDistance, layers, interaction)
//딕션어리
Dictionary<Health, Damageable> uniqueDamagedHealths = new Dictionary<Health, Damageable>();
uniqueDamagedHealths.Add(health, damageable);
//거리 계산
float distance = Vector3.Distance(uniqueDamageable.transform.position, transform.position);
public class DamageArea : MonoBehaviour
- 투사체에 스플 범위(구형)를 구현하기 위한 컴포넌트
- 유니티 엔진에서 지원하는 AnimationCurve 클래스를 사용해 곡선 함수를 적용할 수 있다.
void InflictDamageInArea(float damage, Vector3 center, LayerMask layers,
QueryTriggerInteraction interaction, GameObject owner)
- QueryTriggerInteraction뭔지 부터 설명한다. 이건 쿼리를 발생시킬 경우를 나타낸다.
- QueryTriggerInteraction.Collide로 충돌시 할상 쿼리를 발생하도록 할 수 있다.
- LayerMask 체크할 개체 플래그이다.
- 범위내에 있는 콜리더를 조회한 뒤, 데미지를 적용하지 않을 콜리더를 걸러내고, 데미지를 거리에 따라 적용
//~ProjectileStandard.cs
//hit이 나면 퀴리가 무조건 발생한다.
const QueryTriggerInteraction k_TriggerInteraction = QueryTriggerInteraction.Collide;
//~~~
void OnHit(Vector3 point, Vector3 normal, Collider collider)
{
// damage
if (AreaOfDamage)
{
// area damage
AreaOfDamage.InflictDamageInArea(Damage, point, HittableLayers, k_TriggerInteraction,
m_ProjectileBase.Owner);
}
//~~~
}
https://docs.unity3d.com/kr/530/ScriptReference/QueryTriggerInteraction.html
QueryTriggerInteraction - Unity 스크립팅 API
Overrides the global Physics.queriesHitTriggers.
docs.unity3d.com
- 메서드는 총알이 부딪치고 난뒤 호출되어 부딪친 위치에서 구를 그려 그 구에 닿은 콜라이더를 참조한다.
- 각 콜라이더를 가진 게임오브젝트가 Damageable 컴포넌트를 확인해서 데미지 가능여부를 확인하고,(컴포넌트 자체가 하나의 플래그 역할을 할 수 있다.)
- 추가적으로 거리별로 데미지를 적용하기 위해 일차적으로 콜라이더를 딕션어리에 수집하고 수집된 콜라이더 하나하나 거리를 계산한다.
- 한 반복문에 사용하지 않은건 성능 문제일까?
void OnDrawGizmosSelected()
- 만약 정해진 컬러가 있다면 해당하는 컬러로 원을 그린다.
- 참조가 없는데도 불구하고, 테스트 실행 시 붉은 원이 그려지는데, 아마도 전처리로 뭔가 해둿을 것이다.
Gameplay / ProjectileStandard.cs
투사체 기준, ProjectileBase를 상속받았다.
1인칭 시점에 따른 투사페 위치보정 포함 Update(), 특수 OnShoot()
namespace Unity.FPS.Gameplay
{
public class ProjectileStandard : ProjectileBase
{
[Header("General")]
//투사체의 모양, 프리펩 관련
public float Radius = 0.01f;
public Transform Root;
public Transform Tip;
public float MaxLifeTime = 5f;
public GameObject ImpactVfx;
public float ImpactVfxLifetime = 5f;
public float ImpactVfxSpawnOffset = 0.1f;
public AudioClip ImpactSfxClip;
public LayerMask HittableLayers = -1;
[Header("Movement")]
public float Speed = 20f;
public float GravityDownAcceleration = 0f;
//1인칭시점 관련 투사체 화면 중앙으로 향할지
public float TrajectoryCorrectionDistance = -1;
public bool InheritWeaponVelocity = false;
[Header("Damage")]
public float Damage = 40f;
public DamageArea AreaOfDamage;
[Header("Debug")]
public Color RadiusColor = Color.cyan * 0.2f;
//추적용
ProjectileBase m_ProjectileBase;
Vector3 m_LastRootPosition;
Vector3 m_Velocity;
bool m_HasTrajectoryOverride;
float m_ShootTime;
Vector3 m_TrajectoryCorrectionVector;
Vector3 m_ConsumedTrajectoryCorrectionVector;
List<Collider> m_IgnoredColliders;
//물리 충돌 감지의 쿼리 설정용
const QueryTriggerInteraction k_TriggerInteraction = QueryTriggerInteraction.Collide;
void OnEnable()
new void OnShoot()
void Update()
bool IsHitValid(RaycastHit hit)
void OnHit(Vector3 point, Vector3 normal, Collider collider)
void OnDrawGizmosSelected()
}
}
//활용
// 여기서 new는 의미 없음, 가독성을 중시한것, 부모 클래스에서 OnShoot 딜리게이트가 선언되어 있는데 이와 구별하기 위한것
new void OnShoot()
//시간 지나면 사라짐
Destroy(gameObject, MaxLifeTime);
//위치 이동용, 물리 엔진이 아닌 직접 좌표 이동
m_Velocity = transform.forward * Speed;
//List<T>.AddRange() 리스트에 배열형식의 데이터를 넣을때 사용
m_IgnoredColliders = new List<Collider>();
Collider[] ownerColliders = m_ProjectileBase.Owner.GetComponentsInChildren<Collider>();
m_IgnoredColliders.AddRange(ownerColliders);
//벡터의 합 크기
m_ConsumedTrajectoryCorrectionVector.sqrMagnitude
//벡터의 크기
cameraToMuzzle.magnitude
//방향 벡터(크기1)
cameraToMuzzle.normalized
//Physics.SphereCastAll 메서드
RaycastHit[] hits = Physics.SphereCastAll(m_LastRootPosition, Radius,
displacementSinceLastFrame.normalized, displacementSinceLastFrame.magnitude, HittableLayers,
k_TriggerInteraction);
//회전 데이터 생성용
Quaternion.LookRotation(normal)
class ProjectileStandard : ProjectileBase
- 프리펩 관련 컴포넌트는 기본적으로 몇몇 포인트를 가진다.
- 총알 움직임 관련 변수
- 여러 플래그와 관련된 동작을 구현해서 동작이 길어지는 듯 하다.
void OnEnable()
- 기본 세팅
- 부모 클래스의 OnShoot에 메서드 OnShoot을 구독
- 아마도 OnShoot를 감지하는 클래스와 처리하는 클래스의 구별을 꾀한듯
new void OnShoot()
- 여기서 new 키워드는 가독성을 위한 것
- 발사 시, 필요한 변수 초기화
- 발사체의 콜라이더를 구별하기 위해, 발사체의 콜라이더를 참조해 저장
- 플레이어라면 카메라 시점에 따른 격발시 위치 보정(특수 옵션 TrajectoryCorrectionDistance = 0 or 음수)
- 카메라 시점에 따른 근거리 적 타격 처리
void Update()
- 총알 포지션 이동(물리엔진 사용 x)
- 플레이어라면 카메라 시점에 따른 격발시 위치 보정을 프레임에 맞춰 서서히 적용
- 그 외 위치보정(중력 가속)
- 투사체 끝과 투사체 기준점,
- transform으로 오브젝트의 좌표값을 사용한다면 transform.positon은 프레임에 따른 위치를 자동으로 추적한다.
- 전 프레임의 투사체 기준점, 현프레임의 투사체 끝 부분을 사용해 프레임 간격에 따른 피격판정한다.
bool IsHitValid(RaycastHit hit)
- 피격이 유효한지 판단한다.
- 컴포넌트를 사용한 확인, 무시 콜라인더인지 확인
void OnHit(Vector3 point, Vector3 normal, Collider collider)
- 데미지 적용 방법 호출
- normaldms 이펙트 재생용
Game / IgnoreHitDetection.cs, IgnoreHeatMap.cs
플래그 처럼 사용하는 컴포넌트
namespace Unity.FPS.Game
{
public class IgnoreHitDetection : MonoBehaviour
{
}
}
namespace Unity.FPS.Game
{
public class IgnoreHeatMap : MonoBehaviour
{
}
}
class IgnoreHitDetection : MonoBehaviour, class IgnoreHeatMap : MonoBehaviour
- 컴포넌트 유무를 플래그 처럼 사용한 방법
- 다만 이게 플레그용 컴포넌트인지 확인할 수 있었으면 좋겠다.
- 아마도 무시처리되는 객체가 극소수라서 이런 방식으로 구현했을 것이다.
Game / ConstantRotation.cs
간단한 회전 간단하다.
namespace Unity.FPS.Game
{
public class ConstantRotation : MonoBehaviour
{
[Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;
void Update()
{
// Handle rotating
transform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);
}
}
}
간만에 기본 예제
Game / MinMaxParameters.cs
에디터 struct 입력
namespace Unity.FPS.Game
{
[System.Serializable]
public struct MinMaxFloat
{
public float Min;
public float Max;
public float GetValueFromRatio(float ratio)
{
return Mathf.Lerp(Min, Max, ratio);
}
}
[System.Serializable]
public struct MinMaxColor
{
[ColorUsage(true, true)] public Color Min;
[ColorUsage(true, true)] public Color Max;
public Color GetValueFromRatio(float ratio)
{
return Color.Lerp(Min, Max, ratio);
}
}
[System.Serializable]
public struct MinMaxVector3
{
public Vector3 Min;
public Vector3 Max;
public Vector3 GetValueFromRatio(float ratio)
{
return Vector3.Lerp(Min, Max, ratio);
}
}
}
//사용
//~ ChargedProjectileEffectsHandler.cs
[Tooltip("Scale of the charged object based on charge")]
public MinMaxVector3 Scale;
[Tooltip("Color of the charged object based on charge")]
public MinMaxColor Color;

구조체의 메서드는 정적 메서드가 아니다.
객체가 에디터 입력과 함께 자동으로 생성될것으로 보여진다.
아마 입력을 위해서 작성된 구조체일 것이다.
Game / DebugUtility.cs
디버그용, #if UNITY_EDITOR #endif
namespace Unity.FPS.Game
{
public static class DebugUtility
{
public static void HandleErrorIfNullGetComponent<TO, TS>(Component component, Component source,
GameObject onObject)
{
#if UNITY_EDITOR
if (component == null)
{
Debug.LogError("Error: Component of type " + typeof(TS) + " on GameObject " + source.gameObject.name +
" expected to find a component of type " + typeof(TO) + " on GameObject " +
onObject.name + ", but none were found.");
}
#endif
}
public static void HandleErrorIfNullFindObject<TO, TS>(UnityEngine.Object obj, Component source)
public static void HandleErrorIfNoComponentFound<TO, TS>(int count, Component source, GameObject onObject)
public static void HandleWarningIfDuplicateObjects<TO, TS>(int count, Component source, GameObject onObject)
{
#if UNITY_EDITOR
if (count > 1)
{
Debug.LogWarning("Warning: Component of type " + typeof(TS) + " on GameObject " +
source.gameObject.name +
" expected to find only one component of type " + typeof(TO) + " on GameObject " +
onObject.name + ", but several were found. First one found will be selected.");
}
#endif
}
}
}
디버그 용으로 작성된 클래스, #if UNITY_EDITOR #endif 로 전처리 선택
Game / GameConstants.cs
상수 모음, 대부분 input 관련
namespace Unity.FPS.Game
{
public class GameConstants
{
// all the constant string used across the game
public const string k_AxisNameVertical = "Vertical";
public const string k_AxisNameHorizontal = "Horizontal";
public const string k_MouseAxisNameVertical = "Mouse Y";
public const string k_MouseAxisNameHorizontal = "Mouse X";
public const string k_AxisNameJoystickLookVertical = "Look Y";
public const string k_AxisNameJoystickLookHorizontal = "Look X";
public const string k_ButtonNameAim = "Aim";
public const string k_ButtonNameFire = "Fire";
public const string k_ButtonNameSprint = "Sprint";
public const string k_ButtonNameJump = "Jump";
public const string k_ButtonNameCrouch = "Crouch";
public const string k_ButtonNameGamepadFire = "Gamepad Fire";
public const string k_ButtonNameGamepadAim = "Gamepad Aim";
public const string k_ButtonNameSwitchWeapon = "Mouse ScrollWheel";
public const string k_ButtonNameGamepadSwitchWeapon = "Gamepad Switch";
public const string k_ButtonNameNextWeapon = "NextWeapon";
public const string k_ButtonNamePauseMenu = "Pause Menu";
public const string k_ButtonNameSubmit = "Submit";
public const string k_ButtonNameCancel = "Cancel";
public const string k_ButtonReload = "Reload";
}
}

프로젝트 세팅, input manager 와 관련됨
값 사용시
Input.GetAxis(GameConstants.k_ButtonNameGamepadFire)
Gameplay / Pickup.cs
주울 수 있는 아이템, 상호작용
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(Rigidbody), typeof(Collider))]
public class Pickup : MonoBehaviour
{
//아이템이 둥둥 뜨는 효과, 회전 효과, 소리 효과
public float VerticalBobFrequency = 1f;
public float BobbingAmount = 1f;
public float RotatingSpeed = 360f;
public AudioClip PickupSfx;
public GameObject PickupVfxPrefab;
public Rigidbody PickupRigidbody { get; private set; }
Collider m_Collider;
Vector3 m_StartPosition;
bool m_HasPlayedFeedback;
protected virtual void Start()
void Update()
void OnTriggerEnter(Collider other)
protected virtual void OnPicked(PlayerCharacterController playerController)
public void PlayPickupFeedback()
}
}
//활용
PickupRigidbody = GetComponent<Rigidbody>();
m_Collider = GetComponent<Collider>();
// ensure the physics setup is a kinematic rigidbody trigger
PickupRigidbody.isKinematic = true;
m_Collider.isTrigger = true;
// 삼각함수 사용
float bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;
transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;
class Pickup : MonoBehaviour
- 주울 수 있는 아이템 클래스
- 콜리더를 사용한 물리 접촉으로 획득
virtual void Start()
- 컴포넌트 참조, 초기 설정
void Update()
- 둥둥 뜨는 효과와 회전 효과
void OnTriggerEnter(Collider other)
- 플레이어일 경우만을 적용, 동작 감지, 이벤트 전달
virtual void OnPicked(PlayerCharacterController playerController)
- 하위 클래스에서 확장성을 제공하기 위한 목적이 있는 것 같다.
void PlayPickupFeedback()
- 동작 시 소리, 비주얼 효과 및 플래그 토글
Gameplay / HealthPickup.cs, AmmoPickup.cs, WeaponPickup.cs, JetpackPickup.cs
pickup의 구체화,
namespace Unity.FPS.Gameplay
{
public class HealthPickup : Pickup
{
[Header("Parameters")] [Tooltip("Amount of health to heal on pickup")]
public float HealAmount;
protected override void OnPicked(PlayerCharacterController player)
}
}
namespace Unity.FPS.Gameplay
{
public class AmmoPickup : Pickup
{
public WeaponController Weapon;
public int BulletCount = 30;
protected override void OnPicked(PlayerCharacterController byPlayer)
}
}
namespace Unity.FPS.Gameplay
{
public class WeaponPickup : Pickup
{
public WeaponController WeaponPrefab;
protected override void Start()
protected override void OnPicked(PlayerCharacterController byPlayer)
}
}
namespace Unity.FPS.Gameplay
{
public class JetpackPickup : Pickup
{
protected override void OnPicked(PlayerCharacterController byPlayer)
}
}
//활용
// Set all children layers to default (to prefent seeing weapons through meshes)
foreach (Transform t in GetComponentsInChildren<Transform>())
{
if (t != transform)
t.gameObject.layer = 0;
}
class HealthPickup : Pickup
- Health에 구현된 메서드를 사용함.
class AmmoPickup : Pickup
- WeaponController에 구현된 메서드를 사용함
class WeaponPickup : Pickup
- Start() 에서, 매시를 통해 무기를 보는 것을 방지하기 위해, 하위 오브젝트의 레이어를 수정
- PlayerWeaponsManager에 구현된 메서드를 사용함.
class JetpackPickup : Pickup
- Jetpack에 구현된 메서드르 사용
Gameplay / Jetpack.cs
제트팩 기능, 컴포넌트가 존재하는 상태에서 unlock하는 형태로 구현됨
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(AudioSource))]
public class Jetpack : MonoBehaviour
{
[Header("References")]
public AudioSource AudioSource;
public ParticleSystem[] JetpackVfx;
[Header("Parameters")]
public bool IsJetpackUnlockedAtStart = false;
public float JetpackAcceleration = 7f;
[Range(0f, 1f)]
public float JetpackDownwardVelocityCancelingFactor = 1f;
[Header("Durations")]
public float ConsumeDuration = 1.5f;
public float RefillDurationGrounded = 2f;
public float RefillDurationInTheAir = 5f;
public float RefillDelay = 1f;
[Header("Audio")]
public AudioClip JetpackSfx;
bool m_CanUseJetpack;
PlayerCharacterController m_PlayerCharacterController;
PlayerInputHandler m_InputHandler;
float m_LastTimeOfUse;
public float CurrentFillRatio { get; private set; }
public bool IsJetpackUnlocked { get; private set; }
public bool IsPlayergrounded() => m_PlayerCharacterController.IsGrounded;
public UnityAction<bool> OnUnlockJetpack;
void Start()
void Update()
public bool TryUnlock()
}
}
class Jetpack : MonoBehaviour
- 제트팩 기능을 위한 컴포넌트로, 아이템을 먹을 때 컴포넌트가 부착되는 것이 아니라, 제트팩 기능이 unlock 되는 방식이다.
void Start()
- 초기화, 변수, 컴포넌츠 참조
void Update()
- 플래그(제트책 사용 가능 추적) 매프레임 플레이어가 땅에 닿아있는지 추적
- if 내부에 복잡해게 중첩해서 구현 한 것이 아니라, bool 선언(메서드 내부에) 해서 구현함.
- 유저가 추락하고 있다면 별도의 추력을 제공
- ※연료의 량이 비율일때, 시간/시간으로 소모, 리필되는 비율을 계산할 수 있다.
bool TryUnlock()
- 사용가능 플래그 토글
Gameplay / PositionBobbing.cs
둥둥 뜨는 시각효과
(pickup.cs) 구현은 되어있지만, 컴포넌트를 적용해 효과를 적용한게 아니라, 코드 자체를 가져다 썼다.
컴포넌트에 적용하면 작동은 할것이다.
스킵
Gameplay / ChargedProjectileEffectsHandler.cs
투사체의 모양을 변형시키기 위한 컴포넌트, 발사 시 투사체를 변형시킨다.
namespace Unity.FPS.Gameplay
{
public class ChargedProjectileEffectsHandler : MonoBehaviour
{
public GameObject ChargingObject;
public MinMaxVector3 Scale;
public MinMaxColor Color;
MeshRenderer[] m_AffectedRenderers;
ProjectileBase m_ProjectileBase;
void OnEnable()
void OnShoot()
}
}
//활용
//메시 참조, 공유된 자원을 변경하지 않기위해 Instantiate 사용
m_AffectedRenderers = ChargingObject.GetComponentsInChildren<MeshRenderer>();
foreach (var ren in m_AffectedRenderers)
{
ren.sharedMaterial = Instantiate(ren.sharedMaterial);
}
//로컬 스케일 변경, 비율을 통한 스케일 값 반환 메서드 Scale.GetValueFromRatio(float)
ChargingObject.transform.localScale = Scale.GetValueFromRatio(m_ProjectileBase.InitialCharge);
//매시 컬러 변경, "_Color" 는 쉐이더의 속성이름이다.
foreach (var ren in m_AffectedRenderers)
{
ren.sharedMaterial.SetColor("_Color", Color.GetValueFromRatio(m_ProjectileBase.InitialCharge));
}
class ChargedProjectileEffectsHandler : MonoBehaviour
- 메쉬에 직접 참조해서 색상을 바꾼다.
- 다른 컴포넌트의 참조로 구독만 할뿐이다.
- 에디터로 프리팹을 만져서 할 수 있는 부분을 수치적으로 조작하기 위해 만든 매서드 인거 같다.
void OnEnable()
- 개체가 활성상태가 되면 호출되는 메서드
- 메시를 참조한다. 이때 Instantiate를 사용하는 이유는, 프리팹은 공유된 자원이기 때문에, 변경하려는 오브젝트만 변경하기 위해서이다.
void OnShoot()
- 구독한 딜리게이트 OnShoot가 호출되면, 호출됨
- 크기와 색상 변경
Gameplay / ChargedWeaponEffectsHandler.cs
총기 상호작용 이펙트 , 돌아가는 원반이 구성되는 총
using Unity.FPS.Game;
using UnityEngine;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(AudioSource))]
public class ChargedWeaponEffectsHandler : MonoBehaviour
{
[Header("Visual")]
public GameObject ChargingObject;
public GameObject SpinningFrame;
public MinMaxVector3 Scale;
[Header("Particles")]
public GameObject DiskOrbitParticlePrefab;
public Vector3 Offset;
public Transform ParentTransform;
public MinMaxFloat OrbitY;
public MinMaxVector3 Radius;
public MinMaxFloat SpinningSpeed;
[Header("Sound")]
public AudioClip ChargeSound;
public AudioClip LoopChargeWeaponSfx;
public float FadeLoopDuration = 0.5f;
[Range(1.0f, 5.0f)]
public float MaxProceduralPitchValue = 2.0f;
public GameObject ParticleInstance { get; set; }
ParticleSystem m_DiskOrbitParticle;
WeaponController m_WeaponController;
ParticleSystem.VelocityOverLifetimeModule m_VelocityOverTimeModule;
AudioSource m_AudioSource;
AudioSource m_AudioSourceLoop;
float m_LastChargeTriggerTimestamp;
float m_ChargeRatio;
float m_EndchargeTime;
void Awake()
void SpawnParticleSystem()
public void FindReferences()
void Update()
}
}
//응용
//ParticleSyste의 VelocityOverLifetime 모듈 참조
ParticleSystem.VelocityOverLifetimeModule m_VelocityOverTimeModule;
m_DiskOrbitParticle = ParticleInstance.GetComponent<ParticleSystem>();
m_VelocityOverTimeModule = m_DiskOrbitParticle.velocityOverLifetime;
//참조한 모듈의 속성을 조절
m_VelocityOverTimeModule.orbitalY = OrbitY.GetValueFromRatio(m_ChargeRatio);
//파티클 효과 bool
m_DiskOrbitParticle.gameObject.SetActive(m_WeaponController.IsWeaponActive);
//각도 회전 Quaternion.Euler(x,y,z)
SpinningFrame.transform.localRotation *= Quaternion.Euler(0,
SpinningSpeed.GetValueFromRatio(m_ChargeRatio) * Time.deltaTime, 0);
//Mathf.Clamp(float, min, max), Mathf.Clamp(float, 0, 1)
Mathf.Clamp01((m_EndchargeTime - Time.time - FadeLoopDuration) / FadeLoopDuration);
//Mathf.Lerp(a,b,ratio), ab 사이 ratio에 해당하는 값
m_AudioSourceLoop.pitch = Mathf.Lerp(1.0f, MaxProceduralPitchValue, m_ChargeRatio);

class ChargedWeaponEffectsHandler : MonoBehaviour
- 돌아가는 원반이 구성된 총기 이펙트 관련 클래스
- 거기에 추가될 비주얼 이펙트
void Awake()
- 초기화 및 설정
void SpawnParticleSystem()
- 조작시 나타낼 효과 생성
void FindReferences()
- 원하는 참조 접근
void Update()
- 차징이 찰수록 회전이 빨라지게
Gameplay / WeaponFuelCellHandler.cs
총기 상호작용 이펙트, cell움직임
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(WeaponController))]
public class WeaponFuelCellHandler : MonoBehaviour
{
public bool SimultaneousFuelCellsUsage = false;
public GameObject[] FuelCells;
public Vector3 FuelCellUsedPosition;
public Vector3 FuelCellUnusedPosition = new Vector3(0f, -0.1f, 0f);
WeaponController m_Weapon;
bool[] m_FuelCellsCooled;
void Start()
void Update()
}
}
//활용
//Mathf.InverseLerp(float a, float b, float val) ratio Lerp의 정규화 버전
float value = Mathf.InverseLerp(lim1, lim2, m_Weapon.CurrentAmmoRatio);

class WeaponFuelCellHandler : MonoBehaviour
- 셀을 움직이기 위한 컴포넌트
- 단 발사시에
void Start()
- 초기화
void Update()
- 생각보다 셀 움직이는게, 로컬 좌표계로 조작해서 간단하다.
- Mathf.InverseLerp로 비율을 뽑아내고 그 비율로 Vector3.Lerp로 값을 가져온다.
- 네개의 셀이 각 각 움직이도록, 해당하는 구간을 나누어 적요
- 0 : 0 ~ 1/4, 1 : 1/4 ~ 2/4, 2 : 2/4 ~ 3/4, 3 : 3/4 ~ 4/4
//선언
public GameObject[] FuelCells;
for (int i = 0; i < FuelCells.Length; i++)
{
float length = FuelCells.Length;
float lim1 = i / length;
float lim2 = (i + 1) / length;
float value = Mathf.InverseLerp(lim1, lim2, m_Weapon.CurrentAmmoRatio);
value = Mathf.Clamp01(value);
FuelCells[i].transform.localPosition =
Vector3.Lerp(FuelCellUsedPosition, FuelCellUnusedPosition, value);
}
Gameplay / ProjectileChargeParameters.cs
차지시 무기 성능이 변하는 기능
namespace Unity.FPS.Gameplay
{
public class ProjectileChargeParameters : MonoBehaviour
{
public MinMaxFloat Damage;
public MinMaxFloat Radius;
public MinMaxFloat Speed;
public MinMaxFloat GravityDownAcceleration;
public MinMaxFloat AreaOfEffectDistance;
ProjectileBase m_ProjectileBase;
void OnEnable()
void OnShoot()}
}
}
class ProjectileChargeParameters : MonoBehaviour
- 차지시 데미지 증가를 별도의 컴포넌트로 데미지를 수정하는 방향으로 적용된다.
- 여러 변수가 이 기능을 위해 선언되어서 별도로 구현한 듯 하다.
- Onshoot를 추적하는 것이 아니라 매프레임 탄창비율을 확인하는 방법으로 구현되었다.
void OnEnable()
- OnShoot 딜리게이트 구독
void OnShoot()
- 외부에서 차지 비율을 가져와서, 비율을 사용한 min max 적용
Gameplay / OverheatBehavior.cs
무기 과부화 시
namespace Unity.FPS.Gameplay
{
public class OverheatBehavior : MonoBehaviour
{
[System.Serializable]
public struct RendererIndexData
{
public Renderer Renderer;
public int MaterialIndex;
public RendererIndexData(Renderer renderer, int index)
{
this.Renderer = renderer;
this.MaterialIndex = index;
}
}
[Header("Visual")]
public ParticleSystem SteamVfx;
public float SteamVfxEmissionRateMax = 8f;
//Set gradient field to HDR
[GradientUsage(true)]
public Gradient OverheatGradient;
public Material OverheatingMaterial;
[Header("Sound")]
public AudioClip CoolingCellsSound;
public AnimationCurve AmmoToVolumeRatioCurve;
WeaponController m_Weapon;
AudioSource m_AudioSource;
List<RendererIndexData> m_OverheatingRenderersData;
MaterialPropertyBlock m_OverheatMaterialPropertyBlock;
float m_LastAmmoRatio;
ParticleSystem.EmissionModule m_SteamVfxEmissionModule;
void Awake()
void Update()
}
}
}
//활용
//파티클 시스템의 모듈 접근
ParticleSystem.EmissionModule m_SteamVfxEmissionModule;
//렌더러에서 해당하는 머터리얼을 찾고 머터리얼의 인덱스를 저장하둔다.
foreach (var renderer in GetComponentsInChildren<Renderer>(true))
{
for (int i = 0; i < renderer.sharedMaterials.Length; i++)
{
if (renderer.sharedMaterials[i] == OverheatingMaterial)
m_OverheatingRenderersData.Add(new RendererIndexData(renderer, i));
}
}
//머터리얼 속성 블럭
MaterialPropertyBlock m_OverheatMaterialPropertyBlock;
//머터리얼 속성 수정. 에디터에서 바로 보이는 속성명이 아니라, Emission map 이미지 설정 칸 옆의 색상 설정 칸
m_OverheatMaterialPropertyBlock.SetColor("_EmissionColor", OverheatGradient.Evaluate(1f - currentAmmoRatio));
//생성한 속성 블럭 적용
Renderer.SetPropertyBlock(m_OverheatMaterialPropertyBlock, data.MaterialIndex);
struct RendererIndexData
- 생성자도 같이 구현된 구조체,
class OverheatBehavior : MonoBehaviour
- 가열시 색상 변화를 위한 Gradient와 수정할 머터리얼(인덱스 찾기용)을 넣어준다.
void Awake()
- 설정해논 머터리얼을 사용해서, 렌더러에 해당하는 머터리얼의 인덱스를 조회 및 저장
- 필요한 particle system이 모듈 참조
- 그외 초기화
void Update()
- 찾아논 런더러 머터리얼 정보를 사용해서 해당하는 런더러의 머터리얼을 생성한 머터리얼 설정으로 교체
Gameplay / PlayerCharacterController.cs
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(CharacterController), typeof(PlayerInputHandler), typeof(AudioSource))]
public class PlayerCharacterController : MonoBehaviour
{
[Header("References")]
public Camera PlayerCamera;
public AudioSource AudioSource;
[Header("General")]
public float GravityDownForce = 20f;
//땅에 닿은걸 체크할때 사용할 레이어
public LayerMask GroundCheckLayers = -1;
public float GroundCheckDistance = 0.05f;
[Header("Movement")]
//속도 관련
public float MaxSpeedOnGround = 10f;
public float MovementSharpnessOnGround = 15;
[Range(0, 1)]
public float MaxSpeedCrouchedRatio = 0.5f;
public float MaxSpeedInAir = 10f;
public float AccelerationSpeedInAir = 25f;
public float SprintSpeedModifier = 2f;
public float KillHeight = -50f;
[Header("Rotation")]
public float RotationSpeed = 200f;
[Range(0.1f, 1f)]
public float AimingRotationMultiplier = 0.4f;
[Header("Jump")]
public float JumpForce = 9f;
[Header("Stance")]
//카메라 움직임 관련? Crouch 웅크림
public float CameraHeightRatio = 0.9f;
public float CapsuleHeightStanding = 1.8f;
public float CapsuleHeightCrouching = 0.9f;
public float CrouchingSharpness = 10f;
[Header("Audio")]
public float FootstepSfxFrequency = 1f;
public float FootstepSfxFrequencyWhileSprinting = 1f;
public AudioClip FootstepSfx;
public AudioClip JumpSfx;
public AudioClip LandSfx;
public AudioClip FallDamageSfx;
[Header("Fall Damage")]
public bool RecievesFallDamage;
public float MinSpeedForFallDamage = 10f;
public float MaxSpeedForFallDamage = 30f;
public float FallDamageAtMinSpeed = 10f;
public float FallDamageAtMaxSpeed = 50f;
public UnityAction<bool> OnStanceChanged;
public Vector3 CharacterVelocity { get; set; }
public bool IsGrounded { get; private set; }
public bool HasJumpedThisFrame { get; private set; }
public bool IsDead { get; private set; }
public bool IsCrouching { get; private set; }
// 정조준 시 회전 속도가 바뀌는데,
public float RotationMultiplier
{
get
{
if (m_WeaponsManager.IsAiming)
{
return AimingRotationMultiplier;
}
return 1f;
}
}
Health m_Health;
PlayerInputHandler m_InputHandler;
CharacterController m_Controller;
PlayerWeaponsManager m_WeaponsManager;
Actor m_Actor;
Vector3 m_GroundNormal;
Vector3 m_CharacterVelocity;
Vector3 m_LatestImpactSpeed;
float m_LastTimeJumped = 0f;
float m_CameraVerticalAngle = 0f;
float m_FootstepDistanceCounter;
float m_TargetCharacterHeight;
const float k_JumpGroundingPreventionTime = 0.2f;
const float k_GroundCheckDistanceInAir = 0.07f;
void Awake()
void Start()
void Update()
void OnDie()
void GroundCheck()
void HandleCharacterMovement()
bool IsNormalUnderSlopeLimit(Vector3 normal)
Vector3 GetCapsuleBottomHemisphere()
Vector3 GetCapsuleTopHemisphere(float atHeight)
public Vector3 GetDirectionReorientedOnSlope(Vector3 direction, Vector3 slopeNormal)
void UpdateCharacterHeight(bool force)
bool SetCrouchingState(bool crouched, bool ignoreObstructions)
}
}
//활용
//CharacterController 클래스
CharacterController m_Controller;
PlayerCharacterController : MonoBehaviour
- PlayerInputHandler.cs에서 구현된 PlayerInputHandle가 일차적으로 가공한 인풋으로 플레이거 실제로 움직이는 컴포넌트
- CharacterController 라는 유니티엔진이 제공하는 클래스 사용
'Endeavor > Unity Stuff' 카테고리의 다른 글
Blender remind (0) | 2025.04.29 |
---|---|
유니티 각도 관련 (0) | 2025.04.26 |
EditorWindow simple Exemple (0) | 2025.04.24 |
Unity Learn - Creative Core Pathway - Final Submission (0) | 2024.05.16 |