문제 설명

배열이 하나 주어진다. 그 배열에서 i ~ j번째 수들만 따로 정렬한 뒤, k번째 수를 return해라. 이러한 일련의 과정은 반복되어야 한다. i, j, k에 대한 2차원 배열도 주어진다. 

 

입출력 예

더보기

array 배열이 { 1, 5, 2, 6, 3, 7, 4 }이고,

2차원 commands 배열이 { {2, 5, 3}, {4, 4, 1}, {1, 7, 3} }인 경우,

result 배열은 { 5, 6, 3 }이다.

 

풀이

정렬 문제이다. int 배열을 정렬하는 것이니 특정한 정렬 방식을 사용할 필요는 없어보여서 STL의 sort를 사용했다.

 

원본 배열인 answer를 보존하기 위해서 temp 배열을 하나 선언하고, 거기에 정렬할 값들을 넣는 식으로 구현했다.

 

1. temp배열에 주어진 범위 내의 array 배열의 원소를 넣는다.

 

2. sort()로 정렬한다.

 

3. k번째 수를 answer 배열에 추가한다.

 

코드

#include <string>
#include <vector>
#include <algorithm>
#include <iostream>

using namespace std;

vector<int> solution(vector<int> array, vector<vector<int>> commands)
{
    vector<int> answer;
    vector<int> temp;
    for (int i = 0; i < commands.size(); i++)
    {
        temp.clear();
        for (int j = commands[i][0] - 1; j < commands[i][1]; j++)
        {
            temp.push_back(array[j]);
        }
        sort(temp.begin(), temp.end());
        answer.push_back(temp[commands[i][2] - 1]);
    }
    return answer;
}

int main(void)
{
    vector<int> array = { 1, 5, 2, 6, 3, 7, 4 };
    vector<vector<int>> commands = { {2, 5, 3}, {4, 4, 1}, {1, 7, 3} };
    vector<int> result;
    result = solution(array, commands);
    for (int i = 0; i < result.size(); i++)
    {
        cout << result[i] << endl;
    }
}

문제 설명

스코빌 지수를 나열한 int 배열이 주어진다. 그 배열에서 "특정 스코빌 지수" 2개를 차례대로 꺼내어(=삭제) 이 식에 대입한다; 가장 적은 스코빌 지수 + (그 다음으로 적은 스코빌 지수 * 2). 그 후 이 식으로부터 도출된 값을 다시 그 배열에 넣는다(=추가). 만일 이 배열 내에 있는 모든 스코빌 지수가 K보다 커진다면 이 과정을 종료한 뒤, 몇 번이나 "꺼내고 넣는" 과정을 반복해야 했는지, 그 횟수를 return한다.

 

입출력 예

더보기

scoville = { 1, 2, 3, 9, 10, 12 }

K = 7

결과 : 2

 

그 이유는?

1회차 : 1 + (2 * 2) = 5, --> scoville = { 3, 5, 9, 10, 12 }

2회차 : 3 + (5 * 2) = 13, --> scoville = { 9, 10, 12, 13 }

2회차를 거친 이후엔, 배열 내의 모든 scoville 지수가 K값인 7보다 크다.

 

제한사항 분석

문제에서 주어진 바로는...

scoville의 길이는 2 이상 1,000,000 이하입니다.

K는 0 이상 1,000,000,000 이하입니다.

scoville의 원소는 각각 0 이상 1,000,000 이하입니다.

모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우에는 -1을 return 합니다.

이러한 조건들이 있다. 

 

1. 만일 스코빌 배열의 길이가 2 이상이라는 조건이 없었다면 배열의 길이를 선제적으로 체크하기 위해 do-while문으로 짰겠지만, 그렇지 않았기에 while문으로 했다.

2. K값의 범위는 0 ~ 1,000,000,000인데, max값이 10^9이므로 int로 해도 된다.

3. 이것도 딱히 문제가 되진 않는다.

4. 모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우는 --> 배열 내의 원소의 크기가 모두 K 미만이면서, 원소의 개수가 2 미만일 때이다.

 

그러나 다른 문제와는 다르게 따로 제한사항 분석을 적은 이유는, 한 가지 오류를 생각해낼 수 있기 때문이다. 

만일 K가 10^9인 경우에, 일련의 과정을 통해, 배열 내에 원소가 2개가 남았고, 각각의 원소가 10^9 - 1과 10^9라고 하자. 이런 경우, 모든 원소가 K값 이상이 아니므로 10^9 - 1 + (2 * 10^9)를 한 값을 배열에 넣을 것이다. 그러면 int 오버플로우가 난다. 이렇게 되면 배열 내에 남는 원소는 오롯이 "음수"가 되며, 이는 "모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우"가 된다. (원래는 가능한 경우이다)

 

이러한 문제를 해결하기 위해선 long long을 사용해야 할 것이다.

 

풀이

최소 힙(우선순위 큐)을 사용하는 문제이다.

 

1. scoville 배열에 있는 값들을 차례로 읽어 최소 힙에 넣는다.

 

2. 최소 힙의 맨 위에서 2개의 원소를 읽어서 최솟값들을 구한 뒤, 문제에 나온 식에 따라 연산을 하고, 그 결과를 push를 한다. 물론 사용한 최솟값들은 pop한다.

 

3. 만일 배열 내의 모든 원소가 K 이상이라면 종료하고, (이때, 굳이 O(n)을 소모해서 모든 원소와 K값을 대조할 필요는 없다. 최소 힙이므로 top값만 K와 대조하면 된다)

 

4. 모든 원소가 K 이상이 아닌데, 배열 내의 원소의 개수가 2개 미만이라면 -1을 return한다.

 

코드

#include <string>
#include <vector>
#include <queue>
#include <iostream>

using namespace std;

int solution(vector<int> scoville, int K)
{
    priority_queue<int, vector<int>, greater<int>> mh; // 최소 힙
    for (int i = 0; i < scoville.size(); i++)
        mh.push(scoville[i]);

    int work_count = 0;
    int first, second;
    while (true)
    {
        if (mh.top() >= K) 
        {
            return work_count;
        }
        else if (mh.size() < 2)
        {
            return -1;
        }
        else
        {
            first = mh.top(); mh.pop();
            second = mh.top(); mh.pop();
            mh.push(first + second * 2);
            work_count++;
        }
    }

    return -2; // 절대 발생하면 안 되는 경우
}

int main(void)
{
    vector<int> scoville = { 1, 2, 3, 9, 10, 12 };
    int K = 7;
    cout << solution(scoville, K);
}

오늘 풀어볼 문제는...

https://school.programmers.co.kr/learn/courses/30/lessons/42576

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr


문제 설명

"마라톤에 참여한 사람들의 명단"과 "마라톤을 완주한 사람들의 명단"이 주어진다. 이를 대조하여, 마라톤을 완주하지 못한 사람을 찾아내라. (단, 마라톤을 완주하지 못한 사람의 수는 언제나 1명이다!)


 

풀이

해시맵(unordered_map)을 사용하는 문제이다. 

 

1. "마라톤에 참여한 사람들의 이름"을 해시맵에 추가한 뒤, "마라톤을 완주한 사람들의 이름"을 제거한다.

2. 이때 동명이인이 있을 수 있으므로; 해시맵을 <string, int> 형식으로 만들어야 한다. (이렇게 하는 이유: 이름을 추가할 때는 이를 key로 삼아 +1을, 제거할 때는 -1을 해준 뒤, int 값이 0인 key-value쌍을 해시맵에서 없애버리면 편하기 때문)

3. 마라톤을 완주하지 못한 사람이 한 명뿐이라고 했으므로, 해시맵의 첫 번째 원소를 return한다.


코드

#include <string>
#include <vector>
#include <unordered_map>

using namespace std;

string solution(vector<string> participant, vector<string> completion)
{
    unordered_map<string, int> m;

    for (int i = 0; i < participant.size(); i++)
        m[participant[i]]++;

    for (int i = 0; i < completion.size(); i++)
    {
        m[completion[i]]--;
        if (m[completion[i]] == 0)
            m.erase(completion[i]);
    }

    return m.begin()->first;
}

읽어주셔서 감사합니다!

오늘 풀어볼 문제는...

https://school.programmers.co.kr/learn/courses/30/lessons/1845

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr


문제 설명

N마리의 포켓몬이 주어진다. 플레이어는 주어진 N마리의 폰켓몬 중 최대 N/2마리의 폰켓몬을 고를 수 있다. 이 경우에, 고를 수 있는 폰켓몬-종류의 최댓값을 구하라.


풀이

1. 일단, 고를 수 있는 폰켓몬-종류의 최댓값을 구한다.

--> 폰켓몬 배열 속의 모든 원소들을 set에 넣고, 그 set의 size를 통해 구한다.

 

2. 만일 "고를 수 있는 폰켓몬-종류의 최댓값(set.size)"이 "고를 수 있는 폰켓몬의 수(N/2)"보다 많다면 N/2를 return한다. 그렇지 않다면 그대로 return한다.


코드

#include <vector>
#include <set>

using namespace std;

int solution(vector<int> nums)
{
	set<int> s;
	for (int i = 0; i < nums.size(); i++)
		s.insert(nums[i]);

	if (s.size() > nums.size() / 2)
		return nums.size() / 2;
	else
		return s.size();
}

읽어주셔서 감사합니다!

개념

Object Pooling이란 최적화를 하는 방법 중 하나이다. 주로 생성(Instantiate)과 소멸(Destroy)을 반복하는 오브젝트에 적용한다. 이는 힙 영역에 빈번한 메모리의 할당과 해제가 일어나는 것을 막아줘서, Garbage Collector의 부담을 줄인다.

 

적용 방식

Pool Manager에 모든 Instantiate를 위임한다. (Spawner와 같은 개별 객체가 Instantiate를 하기 위해선 public static PoolManager instance;처럼 선언했다는 가정 하에, PoolManager.instance.MyInstantiate(~~~)처럼, 반드시 PoolManager를 거쳐가야 한다)

 

MyInstantiate()는:

1. 만일 화면 내에 Instantiate를 하고 싶은 객체가 SetActive(false) 상태로 존재한다면? 그걸 SetActive(true)한다. 

2. 만일 화면 내에 Instantiate를 하고 싶은 객체가 없다면? Instantiate()한다. 

 

여기에서 핵심은,

원래는 Destroy(gameObject) 후 Instantiate를 함으로써 Instantiate와 Destroy를 반복하며 객체를 사용했다면,

Object Pooling에서는, 원래는 Destroy()를 할 부분에 SetActive(false)를 넣어서 오브젝트를 재사용한다는 것이다. 

 

Pool Manager

using UnityEngine;
using System.Collections.Generic;

public class PoolManager : MonoBehaviour
{
    public static PoolManager instance;

    private List<GameObject>[] pools; // 한 칸에 리스트 한 개를 담는 2차원 배열
    [SerializeField] private GameObject[] all_prefabs;

    private void Awake()
    {
        instance = this;

        pools = new List<GameObject>[all_prefabs.Length];
        for (int i = 0; i < pools.Length; i++) // prefab의 종류만큼 list를 만든다.
        {
            pools[i] = new List<GameObject>();
        }
    }

    // about "index"
    // Bird(0), Net(1)...
    public GameObject Get(int index)
    {
        GameObject select = null;

        for (int i = 0; i < pools[index].Count; i++)
        {
            if (pools[index][i].activeSelf == false)
            {
                select = pools[index][i];
                select.SetActive(true);
                break;
            }
        }

        if (select == null)
        {
            select = Instantiate(all_prefabs[index], transform);
            pools[index].Add(select);
        }

        return select;
    }

    public GameObject Get(int index, Vector2 position) // overloading, 특정 위치에 스폰해야 하는 경우
    {
        GameObject select = null;

        foreach (GameObject item in pools[index])
        {
            if (item.activeSelf == false)
            {
                select = item;
                select.transform.position = position;
                select.SetActive(true);
                break;
            }
        }

        if (select == null)
        {
            select = Instantiate(all_prefabs[index]);
            select.transform.position = position;
            pools[index].Add(select);
        }

        return select;
    }
}

 

public static PoolManager instance;는 다른 코드에서도 PoolManager를 자유롭게 참조하기 위함이다. 예를 들어 Spawner 코드에서...

 

Spawner

using UnityEngine;
using System.Collections;

public class Spawner : MonoBehaviour
{
    private int level;

    private void Awake()
    {
        level = 1;
    }

    private void Start()
    {
        StartCoroutine(SpawnEnemy());
    }

    IEnumerator SpawnEnemy()
    {
        while (true)
        {
            PoolManager.instance.Get(Random.Range(0, level));
            yield return new WaitForSeconds(1f);
        }
    }
}

 

이런 식으로 쉽게 사용할 수 있다. 여기에선 Get이 MyInstantiate와 같은 역할을 하고 있다. (참고로 0은 Bird 객체를 나타낸다) 그럼 Object Pooling을 적용한 객체는 어떤 식으로 구성되어 있는지 보자.

 

Bird - Enemy 클래스 상속

using UnityEngine;

public class Bird : Enemy
{
    private Rigidbody2D rb;
    private float speed;
    private int lane;

    private void Awake()
    {
        // Enemy class
        max_health = 5;

        // my class
        rb = GetComponent<Rigidbody2D>();
        speed = -5f;
        // lane (매번 OnEnable마다 랜덤으로 할당)
    }

    protected override void Start()
    {
        base.Start();
        lane = Random.Range(-1, 2); // -1 ~ 1
        transform.position = new Vector2(10, lane * 3.5f);
        rb.linearVelocity = new Vector2(speed, 0);
    }

    private void OnEnable()
    {
        Start(); // 이해하기 쉽도록 이렇게 짬. 익숙해지면 Start를 지울 것 같다!
    }

    private void Update()
    {
        if (transform.position.x < -15f)
        {
            gameObject.SetActive(false);
        }
    }
}

 

이 코드를 보면 화면 밖으로 날아갔을 시 SetActive(false)를 하고, SetActive(true)가 된 순간을 OnEnable()을 통해 포착하여 초기화를 해주고 있는 것을 볼 수 있다. (참고로 Enemy class를 상속하고 있다)

 

Enemy (참고용)

using UnityEngine;

public class Enemy : MonoBehaviour
{
    protected float health;
    protected float max_health;
    [SerializeField] private GameObject[] effect_particle;

    protected virtual void Start()
    {
        InitStat();
    }

    private void InitStat()
    {
        health = max_health;
    }

    public void TakeDamage(float damage)
    {
        if (health - damage > 0)
        {
            GenerateEffect("Hit");
            health -= damage;
        }
        else
        {
            Die();
        }
    }

    private void Die()
    {
        GenerateEffect("Die");
        gameObject.SetActive(false);
    }

    private void GenerateEffect(string effect_type)
    {
        if (effect_type == "Hit")
        {
            for (int i = 0; i < 10; i++) { GameObject temp = Instantiate(effect_particle[0], transform.position, transform.rotation); }
        }
        else if (effect_type == "Die")
        {
            for (int i = 0; i < 20; i++) { GameObject temp = Instantiate(effect_particle[1], transform.position, transform.rotation); }
        }
    }
}
using UnityEngine;

public class DoubleLinkedList
{
    private class Node
    {
        public string data;
        public Node prev;
        public Node next;

        public Node(string data)
        {
            this.data = data;
            prev = null;
            next = null;
        }
    }

    public int Length { get; private set; }
    private Node head;
    private Node tail;

    public DoubleLinkedList()
    {
        Length = 0;
        head = null;
        tail = null;
    }

    public void Append(string data)
    {
        Node newNode = new Node(data);
        Length++;

        if (head == null)
        {
            head = newNode;
            tail = newNode;
        }
        else
        {
            tail.next = newNode;
            newNode.prev = tail;
            tail = newNode;
        }
    }

    public int Remove(string value)
    {
        if (Length == 0)
        {
            return -1;
        }
        else if (Length == 1)
        {
            if (IsIncluded(value) == true)
            {
                head = null;
                tail = null;
                Length--;
                return 0;
            }
            else
            {
                return -1;
            }
        }
        else //if Length >= 2
        {
            Node current = head;
            for (int i = 0; i < Length; i++)
            {
                if (current.data == value)
                {
                    if (current == head)
                    {
                        head.next.prev = null;
                        head = head.next;
                        Length--;
                        return 0;
                    }
                    else if (current == tail)
                    {
                        tail.prev.next = null;
                        tail = tail.prev;
                        Length--;
                        return 0;
                    }
                    else
                    {
                        current.next.prev = current.prev;
                        current.prev.next = current.next;
                        Length--;
                        return 0;
                    }
                }
                current = current.next;
            }
            return -1;
        }
    }

    public void Clear() // == RemoveAll()
    {
        // 어짜피 Garbage Collector 때문에 굳이 free를 안 해줘도 다 지워질 운명임.
        Length = 0;
        head = null;
        tail = null;
    }

    public int Insert(string data, int index) // == InsertAfter
    {
        if (index < 0 || index >= Length) // index : 0 ~ (Length - 1), Length >= 1
        {
            return -1;
        }
        else
        {
            Node newNode = new Node(data);
            if (index == 0) // insert after head
            {
                if (head.next == null)
                {
                    Append(data);
                    return 0;
                }
                else
                {
                    newNode.next = head.next;
                    head.next.prev = newNode;
                    head.next = newNode;
                    newNode.prev = head;
                    Length++;
                    return 0;
                }
            }
            else if (index == Length - 1) // insert after tail
            {
                Append(data);
                return 0;
            }
            else // insert at middle
            {
                Node current = head;
                for (int i = 0; i < index; i++)
                {
                    current = current.next;
                }
                newNode.next = current.next;
                current.next.prev = newNode;
                current.next = newNode;
                newNode.prev = current;
                Length++;
                return 0;
            }
        }
    }

    public int Replace(int index, string value)
    {
        if (Length == 0 || index < 0 || index >= Length)
        {
            return -1;
        }
        else 
        {
            Node current = head;
            for (int i = 0; i < index; i++)
            {
                current = current.next;
            }
            current.data = value;
            return 0;
        }
    }

    public string GetData(int index)
    {
        if (Length == 0 || index < 0 || index >= Length)
        {
            return "null";
        }
        else
        {
            Node current = head;
            for (int i = 0; i < index; i++)
            {
                current = current.next;
            }
            return current.data;
        }
    }

    public int GetIndex(string value)
    {
        Node current = head;
        for (int i = 0; i < Length; i++)
        {
            if (current.data == value)
            {
                return i;
            }
            current = current.next;
        }
        return -1;
    }

    // GetLength는 필요없음. 왜냐하면 { get; private set; }을 통해 밖에서도 보이는 값으로 만들어놨기 때문.

    public bool IsIncluded(string value)
    {
        Node current = head;
        for (int i = 0; i < Length; i++)
        {
            if (current.data == value)
            {
                return true;
            }
            current = current.next;
        }
        return false;
    }

    public void PrintList()
    {
        Node current = head;
        for (int i = 0; i < Length; i++)
        {
            Debug.Log(current.data);
            current = current.next;
        }
    }
}

서론

요새 모바일 게임에 관심이 좀 생겼는데, 그러다가 모바일 게임들은 "입력"을 받는 방식이 상당히 한정되어 있다는 사실을 깨달았다. 그래서 터치와 드래그를 구별하는 코드를 한 번 짜봤다. 

 

코드

using UnityEngine;

public class Node : MonoBehaviour
{
    private SpriteRenderer sr;

    [SerializeField] private bool isSelected;
    [SerializeField] private Vector2 startPos;

    private void Awake()
    {
        sr = GetComponent<SpriteRenderer>();

        isSelected = false;
    }

    private void OnMouseDown()
    {
        startPos = (Vector2)Camera.main.ScreenToWorldPoint(Input.mousePosition);
    }

    private void OnMouseUp()
    {
        if ((Vector2)Camera.main.ScreenToWorldPoint(Input.mousePosition) == startPos)
        {
            isSelected = !isSelected;
            if (isSelected)
            {
                sr.color = new Color(1, 1, 1, 1f);
            }
            else
            {
                sr.color = new Color(0, 0, 0, 0.25f);
            }
        }
    }

    private void OnMouseDrag()
    {
        if (isSelected)
        {
            Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            transform.position = new Vector3(mousePosition.x, mousePosition.y, 0);
        }
        else
        {
            return;
        }
    }
}

qsort() 함수의 원형

//아래는 qsort() 함수의 원형

void qsort
(
	void* base, //정렬할 배열의 주소
    size_t element_num, //데이터의 총 개수
    size_t element_size, //개별 데이터의 크기
    int (cdec1* compare)(const void*, const void*) //비교 함수에 대한 포인터
)

 

소스 코드

#include <stdio.h>
#include <stdlib.h> //qsort()

int compare(const void* a, const void* b)
{
	return *(int*)a - *(int*)b;
}

int main(void)
{
	int array[5] = { 1, 5, 3, 4, 2 };
	qsort(array, 5, sizeof(int), compare);
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", array[i]);
	}
}

오늘은...

투혼에서 저그로 컴퓨터(테란) 3마리를 잡아볼 것이다.


전략

  • 생초보인만큼, 클리어만 해보자는 마음으로 "러커 올인" 전략을 사용했다.
  • 9오버풀 -> 빠른 가스 -> 레어 -> 히드라덴 -> 러커 업글 -> 4분 40초 대에 러커 변태 시작!
  • 실력이 조금 더 붙어서 다른 전략도 써볼 수 있게 되면 좋겠다 ㅎㅎ

플레이 영상

https://youtu.be/rCPRimzyxeU

"전략도 실력의 일종이다!"라고 말하고 싶은 생초보입니다 ㅠㅠ

소감

이제 모든 종족으로 투혼 1대3 컴까기를 완수하면, 생초보에서 초보로 승급할 수 있다! (화이팅!)

 

그나저나 "러커 올인" 방식을 제외하고, 실제로 물량을 통한 힘싸움을 하여 1대3을 이길 수 있다면 그건 프로게이머가 아닐까...? 오리지널 & 브루드워 캠페인을 모두 깬 뒤에 다시 한 번 도전해봐야겠다!


읽어주셔서 감사합니다!

간단한 방법 (동시에 플레이되는 소리의 종류가 한 가지밖에 없을 때)

더보기

AudioSource 컴포넌트를 이용하면 된다.

  1. 원하는 GameObject를 선택한 뒤, AudioSource 컴포넌트를 부착한다.
  2. 이 컴포넌트의 Audio Resource 부분에 소리 파일(Audio Clip)을 드래그해 넣는다. (소리 파일의 형식은 주로 .wav나 mp3이다)
  3. 이렇게 하면 플레이(게임 시작) 버튼을 누르자마자 지정한 소리가 나온다.

그러나 위의 간단한 방법에는 상당히 많은 문제가 있는데,

1. 원할 때 소리를 플레이할 수 없다는 것

2. 코드를 좀 짜서, 1번 문제를 해결했다고 하더라도, 두 종류 이상의 효과음을 한 번에 플레이할 수 없다. (한 효과음이 재생되고 있는 도중에, 다른 효과음이 나온다면, 재생되고 있던 효과음이 중단된다.)

 

그렇다면 보다 많은 종류의 효과음이 동시에 플레이될 수 있는 구조를 만들려면 어떻게 해야 할까?

 

 

구체적인 방법 (동시에 플레이되는 소리의 종류가 두 가지 이상일 때)

여러 개의 AudioSource 컴포넌트를 이용하면 된다.

그러나 많은 수의 AudioSource 컴포넌트를 각각의 GameObject에 붙이고, 관리하는 것은 매우 힘들기 때문에, AudioManager 객체를 하나 만들어줘야 한다.

 

다음은 AudioManager 클래스의 코드이다.

using Unity.VisualScripting;
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    [SerializeField] private AudioClip bgmClip;
    private AudioSource bgmPlayer;

    [SerializeField] private AudioClip[] sfxClips;
    private AudioSource[] sfxPlayers;

    private void Awake()
    {
        bgmPlayer = gameObject.AddComponent<AudioSource>();
        bgmPlayer.clip = bgmClip;

        sfxPlayers = new AudioSource[sfxClips.Length];
        for (int i = 0; i < sfxClips.Length; i++)
        {
            sfxPlayers[i] = gameObject.AddComponent<AudioSource>();
            sfxPlayers[i].clip = sfxClips[i];
        }
    }

    private void Start()
    {
        PlayBGM();
    }

    private void PlayBGM()
    {
        bgmPlayer.loop = true;
        bgmPlayer.Play();
    }

    public void PlaySFX(int index)
    {
        sfxPlayers[index].Play();
    }
}

 

 

(참고로 "[SerializedField] private" <-- 이 구절은 private 멤버 변수를 public하게 Inspector 창에서 접근할 수 있도록 만들지만, 멤버 변수 자체는 private의 성질을 지니도록 하는 관용어구...와 같은 것이다. 이 코드를 볼 때는 간단하게 [SerializedField] private == public이라고 생각하면 된다. 아니면 그냥 신경쓰지 않아도 된다! 그냥 아무 의미가 없는 것으로 생각해도 코드를 이해하는 데는 아무런 지장이 없다!)

 

보통 Manager류 코드는 빈 객체(Empty GameObject)에다가 Manager용 스크립트만 부착해서 사용한다. AudioManager도 그런 식으로 사용할 것을 가정하고 만들었다.

 

bgm(배경음악) 관련 부분부터 보면 이해하기 편할 것이다.

 

bgm 관련 부분

더보기
  1. bgmClip 변수를 통해, AudioClip(소리 파일)을 받을 공간을 하나 형성해준다. (이 변수는 [SerializedField] private을 통해 형성되었기 때문에, Inspector 창에서 직접 소리 파일(wav, mp3)을 드래그해 넣을 수 있다.)
  2. bgmPlayer = gameObject.AddComponent<AudioSource>();를 통해 bgmPlayer 변수에 새로 생성한 AudioSource 컴포넌트를 할당(참조)해준다. 참고로 새로 생성한 AudioSource 컴포넌트는 AudioManager GameObject에 붙는다. (아까 말한 빈 객체에 붙는다.)

 

이렇게 하면 게임을 실행하자마자, "bgm용 AudioClip"만을 위한 AudioSource 컴포넌트가 AudioManager GameObject에 생성되게 된다. 

 

그 이후, Start() 메서드와 PlayBGM() 메서드를 통해 배경음악을 재생해주면 된다. (참고로 AudioSource 컴포넌트는, 이 컴포넌트에 할당되어 있는 AudioClip(Audio Resource)을 반복재생(loop)할지 결정할 수 있다. 그게 bgmPlayer.loop = true;로 표현된 것이다.)

 

sfx 관련 부분

(bgm 관련 부분과 거의 같다. bgm은 Audio Clip이 하나고, sfx는 여러 개라는 것이 유일한 차이점이다.)

더보기
  1. sfxClips 배열을 통해, AudioClip(소리 파일)들을 받을 공간을 하나 형성해준다. (이 배열은 [SerializedField] private을 통해 형성되었기 때문에, Inspector 창에서 직접 소리 파일(wav, mp3)을 드래그해 넣을 수 있다.)
  2. sfxPlayers = new AudioSource[sfxClips.Length];를 통해 sfxClip의 개수만큼 sfxPlayer(AudioSource 컴포넌트)를 위한 공간을 만들어준다. --> 이는 각각의 sfxClip을 위한 AudioSource 컴포넌트를 AudioManager에 부착하기 위함이다.
  3. for문을 통해, sfxPlayers[i]에 AudioSource 컴포넌트를 "각각" 할당한다. 그 뒤, sfxPlayers[i]의 Audio Resoure(Audio Clip)을 sfxClips[i]로 설정해준다. 

 

이렇게 하면 게임을 실행하자마자, "sfx용 AudioClip"만을 위한 AudioSource 컴포넌트가 AudioManager GameObject에 생성되게 된다. 

 

그 이후, Start() 메서드와 PlaySFX() 메서드를 통해 배경음악을 재생해주면 된다.

예를 들어, 만약에 Player가 점프할 때 소리를 플레이하고 싶다? 그러면..

  1. private AudioManager am;
  2. private void Awake() { am = FindFirstObjectByType<AudioManager>(); }
  3. public void Jump() { am.PlaySFX(5); }

위와 같은 방식으로 해주면 된다. (만약에 PlaySFX(숫자) 이런 형식이 불편하다면, enum을 사용할 수도 있다.)

 

AudioManager가 완성되면 이런 느낌일 것이다! (오른쪽에 보이는 Inspector 창처럼 되어있을 것이다!)

 

읽어주셔서 감사합니다!

+ Recent posts