https://azln660066.tistory.com/8

 

Particle System 없이 UGUI만으로 이펙트 만들기 - 입문-1 (반짝이효과)

※ 이전에 올라왔던 PPT(https://docs.google.com/presentation/d/1bj__Ei_35hYgkLkpQfm5pSKokUEO2LjWvDczfoU3MHY/edit#slide=id.gee3a1ce05a_0_452)를 기반으로 내용을 그대로 옮겼음을 알려드립니다. 관련 파일 첨부 : 준비물 : 스

azln660066.tistory.com

 

반짝이는 별 효과를 내기 위해서 참고한 블로그

 

애니메이션을 이용한 효과인데 아주 유용한 것 같다

 

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class AudioManager : MonoBehaviour
{
    public static AudioManager instance;
    
    public AudioSource backgroundMusic;
    public AudioClip[] backgroundMusicLists;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            SceneManager.sceneLoaded += OnSceneLoaded;
        }
        else
        {
            Destroy(gameObject);
        }
        backgroundMusic = GetComponent<AudioSource>();
    }

    private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
    {
        for (int i = 0; i < backgroundMusicLists.Length; i++)
        {
            if (scene.name == backgroundMusicLists[i].name)
            {
                PlayBackgroundSound(backgroundMusicLists[i]);
            }
            else
            {
                Debug.Log("씬 이름이 맞지 않거나 해당씬에 맞는 bgm 할당이 되지 않았습니다.");
            }
        }
    }

    public void PlaySfxSound(string sfxName, AudioClip clip)
    {
        GameObject audio = new GameObject(sfxName + "Sound");
        AudioSource audioSource = audio.AddComponent<AudioSource>();
        audioSource.clip = clip;
        audioSource.Play();
        
        Destroy(audio, clip.length);
    }

    public void PlayBackgroundSound(AudioClip clip)
    {
        backgroundMusic.clip = clip;
        backgroundMusic.loop = true;
        backgroundMusic.volume = 1f;
        backgroundMusic.Play();
    }

}

 

사운드를 담당하는 사운드 매니저

 

 public static AudioManager instance;
 
 public AudioSource backgroundMusic;
 public AudioClip[] backgroundMusicLists;

private void Awake()
{
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
        SceneManager.sceneLoaded += OnSceneLoaded;
    }
    else
    {
        Destroy(gameObject);
    }
}

 

다른 클래스에서 접근할 수 있게 싱글톤으로 만들고

 

씬이 로드할때 구독된 OnSceneLoaded 메서드 호출

 

private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
    for (int i = 0; i < backgroundMusicLists.Length; i++)
    {
        if (scene.name == backgroundMusicLists[i].name)
        {
            PlayBackgroundSound(backgroundMusicLists[i]);
        }
        else
        {
            Debug.Log("씬 이름이 맞지 않거나 해당씬에 맞는 bgm 할당이 되지 않았습니다.");
        }
    }
}

 

OnSceneLoaded

Audioclip 배열(backgroundMusicList.Length)의 갯수만큼 반복문을 돌려

 

씬 이름과 배열에 등록된 음악의 인덱스 번호가 동일하면

 

if문 실행, 그렇지 않을 경우 debug.log

 

PlayBackgroundSound 메서드에 배열에 등록된 음악 실행

 

처음에 음악이 실행되지 않아

 

Debug.Log로 backgroundMusicList.Length를 찍었으나 정상적으로 나왔고

 

이후 Debug.Log로 scene.name == backgroundMusicLists[i].name이 맞는지

 

확인했는데 false가 나왔다. 음악파일명과 씬 이름이 달라서 실행되지 않은 문제

 

public void PlayBackgroundSound(AudioClip clip)
{
    backgroundMusic.clip = clip;
    backgroundMusic.loop = true;
    backgroundMusic.volume = 1f;
    backgroundMusic.Play();
}

 

PlayBackgroundSound

Audiosource에 Audioclip을 매개변수로 넘겨주고 

반복해서 실행할 수 있도록 loop를 true로 설정

 

public void PlaySfxSound(string sfxName, AudioClip clip)
{
    GameObject audio = new GameObject(sfxName + "Sound");
    AudioSource audioSource = audio.AddComponent<AudioSource>();
    audioSource.clip = clip;
    audioSource.Play();
    
    Destroy(audio, clip.length);
}

 

PlaySfxSound

 

게임오브젝트를 생성하고

 

생성된 객체에 AudioSoucre 메서드를 추가(AddComponent) 해준다.

 

이후 음악이 종료되면 파괴(종료)

 

다른곳에서 실행할 경우

 

public AudioClip clip;

AudioManager.Instance.PlaySfxSound("실행할 음악 이름", clip)

 

이때 clip에 bgm을 할당해야한다.

 

 

 


Light2D를 이용한 조명 효과, 파티클을 이용한 모닥불 효과


 

파티클 시스템을 이용하여 모닥불 효과가 나오긴 하지만 뭔가 조금 수정이 필요할 것 같다. 

 

Light2D를 활용, 밤 분위기(0D1A33)와 모닥불 근처 조명 효과(FF8033)를 냈다. 


Sorting Group를 활용한 캐릭터 스프라이트 정렬


 

Sorting Group를 활용하여 캐릭터 스프라이트 정렬을 맞췄다.

 

처음엔 컴포넌트로 조절하려 했으나 실시간 변하는 값을 제어하기 위해

 

스크립트로 제어했다.

 

  private readonly SortingGroup sortingGroup;
    private readonly int sortingOrderModifier = -10;
    private readonly float sortingOrderOffest = 0.5f;
    public PlayerWalkState(PlayerStateMachine stateMachine) : base(stateMachine)
    {
        sortingGroup = base.stateMachine.Player.GetComponent<SortingGroup>();
    }

if (sortingGroup != null)
        {
            sortingGroup.sortingOrder = 
                (int)((stateMachine.Player.transform.position.y - sortingOrderOffest) * sortingOrderModifier);
        }

 

플레이어는 이동하므로 update에서 실시간으로 변화하는 값을 제어해줬다.

 

...중략
    private SortingGroup sortingGroup;
    private readonly int sortingOrderModifier = -10;

    private void Start()
    {
        sortingGroup = GetComponent<SortingGroup>();
        if (sortingGroup != null)
        {
            sortingGroup.sortingOrder = (int)(transform.position.y * sortingOrderModifier);
        }

 

NPC는 위치가 고정되므로 y 좌표가 변할일이 없어 처음 Start에서 값을 정해줬다.

 

 

y위치에 따라 정렬이 되는 모습


공격모션 수정


수정전(정면공격)

 

공격모션이 에셋에 적합한게 없어 덮어씌우다 보니 플레이어를 가리는등 어색한 면이 있었지만

 

기존 애니메이션에 position, rotation 값을 수정하여 방향에 맞게 공격이 나가도록 수정했다.

 

수정후(정면공격)
수정후(우측공격)

플레이어 기준 좌측 상단에 배치된 플레이어 상태

 

목표: 플레이어 기본 체력(100)을 바탕으로 몬스터에게 공격을 받으면 체력이 닳아야한다. 

 

우선 체력바 구현을 위해 UI → Slider 생성

 

필요없는 Handle Slide Area는 제거

 

 

Slider의 Value 값(0~1)을 조절하여 HP가 깍히도록 할 예정

 

 

하트 아이콘을 좌측에 배치하고

 

현재 HP와 경계 및 가시성 향상을 위해 Background를 검은색으로(화살표 표시)

 

HP 영역은 Background 보다 작게 설정(좌, 우, 위, 아래)

 

체력을 표시할 HealthText는 UI 기준 우측하단에 배치하면 UI 설정은 끝난다.

 

public class PlayerStatData
{
    [field: SerializeField] public float MaxHealth { get; private set; } = 100f;
    [field: SerializeField] public float CurrentHealth { get; private set; } = 100f;
}

public class PlayerSO : ScriptableObject
{
    ... 중략
    [field: SerializeField] public PlayerStatData StatData { get; private set; }
}

 

최대 체력을 설정하여 ScriptableObject에서 관리한다.

 

PlayerCondtion.cs를 만들어 플레이어에 붙혀주고

 

public class PlayerCondition : MonoBehaviour
{
    public PlayerStatData playerStatData;
    public BugStats bugStats;
    public Slider healthBar;
    public TextMeshProUGUI healthText;
    
    private float currentHealth;
    private float maxHealth;

    private void Start()
    {
        currentHealth = playerStatData.CurrentHealth;
        maxHealth = playerStatData.MaxHealth; 
    }

    private void Update()
    {
        HpUpdate();
    }

    public void HpUpdate()
    {
        if (healthBar != null)
        {
            healthBar.value = currentHealth / maxHealth;
            healthText.text = $"{currentHealth}/{maxHealth}";
        }
    }

    public float GetCurrentHealth()
    {
        return currentHealth;
    }

    public void TakeDamage(float damage)
    {
        currentHealth -= damage;
        Debug.Log($"플레이어가 {bugStats.bugName}에게 {damage}의 공격을 받았습니다. 현재 체력: {currentHealth}");

        if (currentHealth < 0)
        {
            Destroy(gameObject);
            Debug.Log("사망");
        }
    }
}

 

currentHealth, maxHealth 변수를 선언해서

 

시작시 초기화를 진행 update에서 변화된 value값을 반영해준다.

 

GetCurrentHealth() 메서드는 몬스터가 공격시 현재 체력을 호출하기 위한 메서드

 

TakeDamage 메서드는 몬스터에게 공격받을 시 체력 감소 메서드

 

using Unity.VisualScripting;
using UnityEngine;

public class BugAttackingState : BugState
{
    private BugStats bugStats;
    private float attackCoolTime = 2.0f;
    private float lastAttackTime;
    public override void Enter()
    {
        bugStats = bug.bugStats;
        Debug.Log("버그 공격");
        lastAttackTime = 0;
    }

    public override void Update()
    {
        lastAttackTime += Time.deltaTime; // 경과시간누적
        if (lastAttackTime > attackCoolTime) // 쿨타임이 지났는지 확인
        {
            PlayerAttackDamage();
            lastAttackTime = 0; // 마지막 공격 시간 업데이트
            bug.ChangeState(bug.chasingState); 
            Debug.Log("플레이어 공격중");
        }
    }

    public override void Exit() { }
    ... 중략
}

 

몬스터 공격 상태 패턴 스크립트

 

가장 어려웠던 부분은 Update 부분인데

 

update에서 호출하다보니 몬스터가 공격을 하는 조건을 걸어야 했는데

(그렇지 않으면 매프레임마다 공격하므로 플레이어 바로 die..)

 

처음에는 Time.time(에디터 실행후 경과시간)을 기준으로 로직을 구성했으나

 

여전히 의도대로 되지않아 튜터님께 Help 요청..

 

결론은 debug 결과 실제로는 생각대로 작동하지 않았다.

(내가 생각한 것과 다를 수 있으니 Debug의 중요성을 말씀하심)

 

Time.deltaTime(이전 프레임이 끝나고 현재 프레임 시작되기까지 걸린 시간)으로 로직을 수정했다.


관련 코드 해석

 

lastAttackTime(마지막 공격 이후 시간)을 처음 진입시 0으로 초기화 해주고

 

  update에서 매프레임마다 Time.deltatime을 누적하고

 

attackCoolTime 보다 커지면 공격 실행 후 lastAttackTime 0으로 초기화 되면서

 

추적 로직으로 전환, 이후 2초 이상 경과시 다시 공격모드로 전환되는 원리


 

 

ESC 키를 누를시 설정창이 나오는 UIPausePopup 창

 

그러나 해당 오브젝트가 Hierachy에 없으면 ESC UI를 활성화 할 수 없다.

(즉, 끄고 켤 수 없다.)

 

그래서 우선 플레이어에서 Resources.Load를 통해 동적 생성하는 방식으로 가져가기로 했다.

 

public UIPausePopup currentPausePopup;
private const string filePath = "Prefabs/UI/Popup/UIPausePopup";

public void EscPopupInput()
    {
        if (currentPausePopup == null)
        {
            UIPausePopup prefab = Resources.Load<UIPausePopup>(filePath);
            currentPausePopup = Instantiate(prefab);
        }
        else
        {
            Destroy(currentPausePopup.gameObject);
            currentPausePopup = null;
        }
    }

 

 

UIPausePopup을 참조해서

 

null이면 Resources.Load를 통해 프리팹을 로드해주고

 

이것을 Instantiate해서 currentPausePopup에 저장해준다.

 

Instantiate 한다고 자동으로 저장되지 않고 저장할 위치를 명확하게 지정해야 한다.

 

그리고 Instantiate(prefab)과 currentPausePopup 타입은 같아야된다. ★ 중요 ★

 

만약 null이 아닐경우 해당 게임오브젝트를 파괴해서(On / Off) 기능이 되도록 한다.

 

 

ESC 기능을 인풋시스템에서 추가해주고(button 타입)

 

BaseState.cs
protected virtual void AddInputActionsCallback()
    {
        PlayerController input = stateMachine.Player.input;
        ...중략
        input.playerActions.ESC.started += OnEscStarted;
    }

    protected virtual void RemoveInputActionsCallback()
    {
        PlayerController input = stateMachine.Player.input;
        ... 중략
        input.playerActions.ESC.canceled -= OnEscCanceld;
    }
    
    private void OnEscStarted(InputAction.CallbackContext context)
    {
        stateMachine.Player.EscPopupInput();
    }

 

상태패턴 기본이 되는 baseState에서 이벤트를 등록해주고 EscPopupInput 메서드를 호출하면 끝

+ Recent posts