[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 39회차 미션 시작합니다.

04. 배틀로얄 - 23, 24 번을 진행합니다.


저번 시간에 이어서 SoundTool 스크립트를 지속 작성합니다 ^^


public void FadeTo(Soundclip clip, float time, Interpolate.EaseType ease) {
    if (currentPlayingType == MusicPlayingType.None) {
        FadeIn(clip, time, ease);
    }
    else if (IsDifferentSound(clip)) {
    


캡쳐 화면을 넣을 것이 부족하여 중간중간 화면 캡쳐라도 해서 넣고 있습니다 ^^ 였는데, 오늘은 뒤에 가니 많네요 ㅎㅎ

        if (currentPlayingType == MusicPlayingType.SourceA) {
            PlayAudioSource(fadeB_audio, currentSound, 0.0f);
            currentPlayingType = MusicPlayingType.AtoB;
        }
        else if (currentPlayingType == MusicPlayingType.SourceB) {
            PlayAudioSource(fadeA_audio, currentSound, 0.0f);
            currentPlayingType = MusicPlayingType.BtoA;
        }
        if (currentSound.HasLoop()) {
            isTicking = true;
            DoCheck();
        }
    }
}

public void FadeTo(int index, float time, Interpolate.EaseType ease) {
    FadeTo(DataManager.SoundData().GetCopy(index), time, ease);
}




public void PlayBGM(int index) {
    SoundClip clip = DataManager.SoundData().GetCopy(index);
    PlayBGM(clip);
}

public void PlayUISound(SoundClip clip) {
    PlayAudioSource(UI_audio, clip, clip.maxVolume);
}

public void PlayEffectSound(SoundClip clip) {
    bool isPlaySuccess = false;
    for (int i = 0; i < EffectChannelCount; i++) {
        if (effect_audios[i].isPlaying == false) {
            PlayAudioSource(effect_audios[i], clip, clip.maxVolume);
            effect_PlayStartTime[i] = Time.realtimeSinceStartup;
            isPlaySuccess = true;
        }
        else if (effect_audios[i].clip == clip.GetClip()) {
            effect_audios[i].Stop();
            PlayAudioSource(effect_audios[i], clip, clip.maxVolume);
            effect_PlayStartTime[i] = Time.realtimeSinceStartup;
            isPlaySuccess = true;
        }
    }
    
    if (isPlaySuccess == false) {
        float maxTime = 0.0f;
        int selectIndex = 0;
        for (int i = 0; i < EffectChannelCount; i++) {
            if (effect_PlayStartTime[i] > maxTime) {
                maxTime = effect_PlayStartTime[i];
                selectIndex = i;
            }
        }
        PlayAudioSource(effect_audios[selectIndex], clip, clip.maxVolume);
    }
}

 


PlayEffectSound()와 거의 유사한데, PlayAudioSource() -> PlayAudioSourceAtPoint()로 변경해주면 됩니다.
대신 isPlaySuccess가 실패시 maxTime 체크하던 부분은 필요없이 바로 함수 호출만 해주면 됩니다.


public void PlayOneShotEffect(int index, Vector3 position, float volume) {
    if (index == (int)SoundList.None) return;
    SoundClip clip = DataManager.SoundData().GetCopy(index);
    if (clip == null) return;
    PlayEffectSound(clip, position, volume);
}



public void Stop(bool allStop = false) {
    if (allStop) {
        fadeA_audio.Stop();
        fadeB_audio.Stop();
    }
    
    FadeOut(0.5f, Interpolate.EaseType.Linear);
    currentPlayingType = MusicPlayingType.None;
    StopAllCoroutines();
}

여기까지가 SoundTool의 구현이 끝입니다 ^^~ 수고하셨습니다.
지금까지는 무언가 싶지만 Player나 Monster 작업을 할 때 지금 만들어놓은 것들이 힘을 발휘하게 됨을 볼 수 있습니다.
자세한 설명들도 직접 적용하는 시점에 하기 위해 코딩에 집중하였다고 하네요.. 어쩐지 너무 타이핑 따라하기 느낌어어서 ㅠ.,ㅜ; 설명도 없고.. ㅎㅎ 무언지 알것은 같은데 사용처를 모르니.. 쩝..
이제부터는 3D GameObject를 가지고 움직이는 기능 구현을 시작합니다.






PluggableBehaviour Pattern을 적용하여 플레이어를 적용할 예정입니다.
우선 카메라 제작을 시작합니다.




배틀그라운드나 바이오해저드와 같은 류의 게임을 보면 어깨 뒤쪽 쯤에서 바라보는 카메라를 배치하게 되는데 이것 "3인칭 Shoulder View"입니다.

숄더뷰의 특징이 여러가지 있습니다. 게임을 해보신 분들은 무슨 느낌인지 바로 압니다. ㅎㅎ 왜냐면 숄더뷰 카메라로 인해 생동감도 넘치는 효과가 있고, 좁은 공간에 있으면 숄더뷰가 캐릭터에 가까이 가게 되어 긴장감도 높여주고 하니까 말이죠..
캐릭터와 카메라 사이에 벽이 있으면 카메라가 캐릭터에 거의 붙도록 다가간다는 특징이 있습니다.

캐릭터와 카메라의 충동 체크는 한쪽에서만 진행을 하게되는 경우 체크가 실패할 수가 있기 때문에 양쪽방향으로 체크를하게 됩니다.

 



"Double Viewing Check"라고 부릅니다.

New Scene을 하나 추가하고, Plane을 추가하여 바닥면을 하나 생성해줍니다.
숄더뷰 카메라는 별도로 생성해도 되지만 이미 존재하는 Main Camera를 숄더뷰로 사용합니다.




ThirdPersonOrbitCam 스크립트를 생성하고 코드를 작성합니다.

// 카메라 속성중 중요 속성 하나는 카메라로부터 위치 오프셋 벡터, 피봇 오프셋 벡터
// 위치 오프셋 벡터는 충돌 처리용으로 사용하고 피봇 오프셋 벡터는 시선이동에 사용하도록
// 충돌체크 : 이중 충돌 체크 기능 (캐릭터->카메라, 카메라->캐릭터)
// 사격 반동을 위한 기능
// FOV 변경 기능.
public class ThirdPersonOrbitCam : MonoBehaviour
{
    public Transform player;
    public Vector3 pivotOffset = new Vector3(0.0f, 1.0f, 0.0f);
    public Vector3 camOffset = new Vector3(0.4f, 0.5f, -2.0f);
    
    public float smooth = 10f; // 카메라 반응속도
    public float horizontalAimingSpeed = 6.0f; // 수평 회전속도
    public float verticalAimingSpeed = 6.0f;
    public float maxVerticalAngle = 30.0f;
    public float minVerticalAngle = -60.0f;
    public float recoilAngleBound = 5.0f; // 사격반동 바운스값
    private float angleH = 0.0f; // 마우스 이동에 따른 카메라 수평이동 수치.
    private float angleV = 0.0f;
    private Transform cameraTransform; // 카메라 트랜스폼 캐싱.
    private Camera myCamera;
    private Vector3 relCameraPos; // 플레이어로부터 카메라까지의 벡터.
    private float relCameraPosMag; // 플레이어로부터 카메라사이의 거리.
    private Vector3 smoothPivotOffset; // 카메라 피봇용 보간 벡터
    private Vector3 smoothCamOffset; // 카메라 위치용 보간 벡터
    private Vector3 targetPivotOffset; // 카메라 피봇용 보간 벡터.
    private Vector3 targetCamOffset; // 카메라 위치용 보간 벡터
    private float defaultFOV; // 기본 시야값
    private float targetFOV; // 타겟 시야값
    private float targetMaxVerticleAngle; // 카메라 수직 최대 각도
    private float recoilAngle = 0f; // 사격 반동 각도
    
    public float GetH { get => angleH; } // return angleH; 같은 코드.
}

위까지가 3인칭 숄더뷰를 위한 기본 변수들의 준비를 마쳤습니다.
다음 시간에는 카메라를 구동하는 함수들을 작성하게 됩니다.

FPS 게임이라는 특성으로 인한 카메라 구현 내용이 있고, FPS 특성으로인해 3D 멀미 등을 유발할 수 있기 때문에 이를 방지하기 위한 변수들과 함수들이 존재하게 되는 것 같습니다 ^^.





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0


 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 38회차 미션 시작합니다.

04. 배틀로얄 - 21, 22 번을 진행합니다.




저번 시간에 이어 SoundManager 스크립트를 지속 작업합니다.

public class SoundManager : SingletonMonobehaviour<SoundManager>
{
    // 저번시간 작성했던 변수들..
    
    void Start() {
        if (mixer == null) {
            mixer = Resources.Load(MixerName) as AudioMixer;
        }
        if (audioRoot == null) {
            audioRoot = new GameObject(ContainerName).transform;
            audioRoot.SetParent(transform);
            audioRoot.localPosition = Vector3.zero;
        }
        if (fadeA_audio == null) {
            GameObject fadeA = new GameObject(FadeA, typeof(AudioSource));
            fadeA.transform.SetParent(audioRoot);
            fadeA_audio = fadeA.GetComponent<AudioSource>();
            fadeA_audio.playOnAwake = false;
        }
        if (fadeB_audio == null) {
            GameObject fadeB = new GameObject(FadeB, typeof(AudioSource));
            fadeB.transform.SetParent(audioRoot);
            fadeB_audio = fadeB.GetComponent<AudioSource>();
            fadeB_audio.playOnAwake = false;
        }
        if (UI_audio == null) {
            GameObject ui = new GameObject(UI, typeof(AudioSource));
            ui.transform.SetParent(audioRoot);
            UI_audio = ui.GetComponent<AudioSource>();
            UI_audio.playOnAwake = false;
        }
        if (effect_audios == null || effect_audios.Length == 0) {
            effect_PlayStartTime = new float[EffectChannelCount];
            effect_audios = new AudioSource[EffectChannelCount];
            for (int i = 0; i < EffectChannelCount; i++) {
                effect_PlayStartTime[i] = 0.0f;
                GameObject effect = new GameObject("Effect" + i.ToString(), typeof(AudioSource));
                effect.transform.SetParent(audioRoot);
                effect_audios[i] = effect.GetComponent<AudioSource>();
                effect_audios[i].playOnAwake = false;
            }
        }
        
        if (mixer != null) {
            fadeA_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(BGMGroupName)[0];
            fadeA_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(BGMGroupName)[0];
            UI_audio.outputAudioMixerGroup = mixer.FindMatchingGroups(UIGroupName)[0];
            for (int i = 0; i < effect_audios.Length; i++) {
                effect_audios[i].outputAudioMixerGroup = mixer.FindMatchingGroups(EffectGroupName)[0];
            }
        }
        
        VolumeInit();
    }
    
    public void SetBGMVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(BGMVolumeParam, volume);
        PlayerPrefs.SetFloat(BGMVolumeParam, volume);
    }
    
    public float GetBGMVolume() {
        if (PlayerPrefs.HasKey(BGMVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(BGMVolumeParam));
        else
            return maxVolume;
    }
    
    public void SetEffectVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(EffectVolumeParam, volume);
        PlayerPrefs.SetFloat(EffectVolumeParam, volume);
    }
    
    public float GetEffectVolume() {
        if (PlayerPrefs.HasKey(EffectVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(EffectVolumeParam));
        else
            return maxVolume;
    }
    
    public void SetUIVolume(float currentRatio) {
        currentRatio = Mathf.Clamp01(currentRatio);
        float volume = Mathf.Lerp(minVolume, maxVolume, currentRatio);
        mixer.SetFloat(UIVolumeParam, volume);
        PlayerPrefs.SetFloat(UIVolumeParam, volume);
    }
    
    public float GetUIVolume() {
        if (PlayerPrefs.HasKey(UIVolumeParam))
            return Mathf.Lerp(minVolume, maxVolume, PlayerPrefs.GetFloat(UIVolumeParam));
        else
            return maxVolume;
    }
    

 



여기까지가 기본적인 사운드 초기화 및 초기 설정을 하는 코드입니다. 이어서 소리 켜고 끄기 및 Fade In/Out 등을 작성합니다.



    void PlayAudioSource(AudioSource source, SoundClip clip, float volume) {  // 소리 재생
        if (source == null || clip == null) return;
        source.Stop();
        source.clip = clip.GetClip();
        source.volume = volume;
        source.loop = clip.isLoop;
        source.pitch = clip.pitch;
        source.dopplerLevel = clip.dopplerLevel;
        source.rolloffMode = clip.rolloffMode;
        source.minDistance = clip.minDistance;
        source.maxDistance = clip.maxDistance;
        source.spartialBlend = clip.spartialBlend;
        source.Play();
    }
    
    void PlayAudioSourceAtPoint(SoundClip clip, Vector3 position, float volume) { // 특정 위치에서 재생
        AudioSource.PlayClipAtPoint(clip.GetClip(), position, volume);
    }
    
    public bool IsPlaying() {
        return (int)currentPlayingType > 0;
    }
    
    public bool IsDifferentSound(SoundClip clip) {
        if (clip == null) return false;
        if (currentSound != null && currentSound.realId == clip.realId && IsPlaying() && currentSound.isFadeOut == false) return false;
        else return true;
    }
   
    private IEnumerator CheckProcess() {
        while (isTicking == true && IsPlaying() == true) {
            yield return new WaitForSeconds(0.05f);
            if (currentSound.HasLoop()) {
                if (currentPlayingType == MusicPlayingType.SourceA) {
                    currentSound.CheckLoop(fadeA_audio);
                }
                else if (currentPlayingType == MusicPlayingType.SourceB) {
                    currentSound.CheckLoop(fadeB_audio);
                }
                else if (currentPlayingType == MusicPlayingType.AtoB) {
                    lastSound.CheckLoop(fadeA_audio);
                    currentSound.CheckLoop(fadeB_audio);
                }
                else if (currentPlayingType == MusicPlayingType.BtoA) {
                    lastSound.CheckLoop(fadeB_audio);
                    currentSound.CheckLoop(fadeA_audio);
                }
            }
        }
    }

 


DoCheck, FadeIn 함수도 위와 같이 구현해 줍니다.

    public void FadeIn(int index, float time, Interpolate.EaseType ease) {
        FadeIn(DataManager.SoundData().GetCopy(index), time, ease);
    }
    
    public void FadeOut(float time, Interpolate.EaseType ease) {
        if (currentSound != null)
            currentSound.FadeOut(time, ease);
    }
   
    void Update() {
        if (currentSound == null) return;
        if (currentPlayingType == MusicPlayingType.SourceA) {
            currentSound.DoFade(Time.deltaTime, fadeA_audio);
        }
        else if (currentPlayingType == MusicPlayingType.SourceB) {
            currentSound.DoFade(Time.deltaTime, fadeB_audio);
        }
        else if (currentPlayingType == MusicPlayingType.AtoB) {
            lastSound.DoFade(Time.deltaTime, fadeA_audio);
            currentSound.DoFade(Time.deltaTime, fadeB_audio);
        }
        else if (currentPlayingType == MusicPlayingType.BtoA) {
            lastSound.DoFade(Time.deltaTime, fadeB_audio);
            currentSound.DoFade(Time.deltaTime, fadeA_audio);
        }
        
        if (fadeA_audio.isPlaying && fadeB_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.SourceA;
        }
        else if (fadeB_audio.isPlaying && fadeA_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.SourceB;
        }
        else if (fadeA_audio.isPlaying == false && fadeB_audio.isPlaying == false) {
            currentPlayingType = MusicPlayingType.None;
        }
    }
}


사실 이런 구현은 말이 많을 것 같다는 느낌도 들긴 하네요. 분명 위와 같은 기능을 하는 툴이 있어야 함은 분명한데..
매번 툴을 만들때마다 많은 시간이 들어가야 하며 코드 관리도 힘들다면 오히려 짐이 될 것도 같기 때문입니다.

다음 시간에는 사운드 매니저를 마무리하고, 3인칭 카메라 제작에 들어갑니다 ^^~




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 37회차 미션 시작합니다.

04. 배틀로얄 - 19, 20 번을 진행합니다.




드디어 SoundTool입니다 ^^~

EffectTool과 비교한다면 속성 및 설정값이 많기에 조금 복잡해 보이는 코드가 되기 하겠지만 핵심적인 내용은 비슷하므로 천천히 따라가면 충분히 이해할 수 있습니다.




해당 위치에 SoundTool Script를 생성하고 코드 작업을 시작합니다.


public class SoundTool : EditorWindow
{
    public int uiWidthLarge = 450;
    public int uiWidthMiddle = 300;
    public int uiWidthSmall = 200;
    private int selection = 0;
    private Vector2 SP1 = Vector2.zero;
    private Vector2 SP2 = Vector2.zero;
    private AudioClip soundSource;
    private static SoundData soundData;
    
    [MenuItem("Tools/Sound Tool")]
    static void Init() {
        soundData = CreateInstance<SoundData>();
        soundData.LoadData();
        
        SoundTool window = GetWindow<SoundTool>(false, "Sound Tool");
        window.Show();
    }
    
    private void OnGUI() {
        if (soundData == null) return;
        EditorGUILayout.BeingVertical();
        {
            UnityObject source = soundSource;
            SoundClip sound = soundData.soundClips[selection];
            EditorHelper.EditorToolTopLayer(soundData, ref selection, ref source, uiWidthMiddle);
            soundSource = (AudioClip)source;
           
            EditorGUILayout.BeingVertical();
            {
                EditorHelper.EditorToolListLayer(ref SP1, soundData, ref selection, ref source, uiWidthMiddle);
                soundSource = (AudioClip)source;
                
                EditorGUILayout.BeingVertical();
                {
                    SP2 = EditorGUILayout.BeginScrollView(SP2);
                    {
                        if (soundData.GetDataCount() > 0) {
                            EditorGUILayout.BeginVertical();
                            {
                                EditorGUILayout.Separator();
                                SoundClip sound = soundData.soundClips[selection];
                                EditorGUILayout.LabelField("ID", selection.ToString(), GUILayout.Width(uiWidthLarge));
                                soundData.names[selection] = EditorGUILayout.TextField("Name", soundData.names[selection]), GUILayout.Width(uiWidthLarge));
                                sound.playType = (SoundPlayType)EditorGUILayout.EnumPopup("PlayType", sound.playType, GUILayout.Width(uiWidthLarge));
                                sound.maxVolume = EditorGUILayout.FloatField("Max Volume", sound.maxVolume, GUILayout.Width(uiWidthLarge));
                                sound.isLoop = EditorGUILayout.Toggle("LoopClip", sound.isLoop, GUILayout.Width(uiWidthLarge));
                                EditorGUILayout.Separator();
                                if (soundSource == null && sound.clipName != string.Empty) {
                                    soundSource = Resources.Load(sound.clipPath + sound.clipName) as AudioClip;
                                }
                                soundSource = (AudioClip)EditorGUILayout.ObjectField("Audio Clip", soundSource, typeof(AudioClip), false, GUILayout.Width(uiWidthLarge));
                                if (soundSource != null) {
                                    sound.clipPath = EditoHelper.GetPath(soundSource);
                                    sound.clipName = soundSource.name;
                                    sound.pitch = EditorGUILayout.Slider("Pitch", sound.pitch, -3.0f, 3.0f, GUILayout.Width(uiWidthLarge));
                                    
                                    // OMG



                                }
                                else {
                                    sound.clipName = string.Empty;
                                    sound.clipPath = string.Empty;
                                }
                                
                                EditorGUILayout.Separator();
                                if (GUILayout.Button("Add Loop", GUILayout.Width(uiWidthMiddle)) {
                                    soundData.soundClips[selection].AddLoop();
                                }
                                
                                // 더이상 따라가기가 벅차네요 ^^;


                            }
                            EditorGUILayout.EndVertical();
                        }
                    }
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndVertical();
        }
        EditorGUILayout.EndVertical();
        
        EditorGUILayout.Separator();
       
        EditorGUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("Reload")) {
                soundData = CreateInstance<SoundData>();
                soundData.LoadData();
                selection = 0;
                soundSource = null;
            }
            if (GUILayout.Button("Save")) {
                soundData.SaveData();
                CreateEnumStructure();
                AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            }
        }
        EditorGUILayout.EndHorizontal();
    }
    
    public void CreateEnumStructure() {
        string enumName = "SoundList";
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < soundData.ames.Length; i++) {
            if (!soundData.names[i].ToLower().Contains("none")) {
                builder.AppendLine("    " + soundData.names[i] + " = " + i.ToString() + ",");
            }
            EditorHelper.CreateEnumStructure(enumName, builder);
        }
    }
}




이렇게하여 SoundTool 창이 잘 떠서 동작함을 확인할 수 있습니다.
상세한 사용 설명은 게임을 개발할 때 설정해 보도록 할 예정입니다.





Audio Mixer에 대한 Unity 화면입니다.





SoundManager의 변수들입니다.

public class SoundManager : SingletonMonobehaviour<SoundManager>
{
    // 위의 변수들..
    
    public enum MusicPlayingType {
        None = 0,
        SourceA = 1,
        SourceB = 2,
        AtoB = 3,
        BtoA = 4
    }
    public AudioMixer mixer = null;
    public Transform audioRoot = null;
    public AudioSource fadeA_audio = null;
    public AudioSource fadeB_audio = null;
    public AudioSource[] effect_audios = null; // 예전 5채널 소리째짐을 회피하기 위한 채널갯수 제한.
    public AudioSource UI_audio = null;
    
    public float[] effect_PlayStartTime = null;
    private int EffectChannelCount = 5;
    private MusicPlayingType currentPlayingType = MusicPlayingType.None;
    private bool isTicking = false;
    private SoundClip currentSound = null;
    private SoundClip lastSound = null;
    private float minVolume = -80.0f;
    private float maxVolume = 0.0f;
}


여기까지가 SoundManager의 필요한 변수들을 설정한 내용입니다 ^^;;;
미리 말씀은 해주셨지만 진짜 멘붕이 오네요. ㅋㅋㅋ 나름 괘않을거라 생각하며 열심히 따라하였으나 중간에 왔다갔다 및 버그 수정 등 따라갈 수가 없는 ㅎㅎㅎㅎ

역시나 옵션이 많다보니 타이핑 신공으로 정신없이 쳐야한다는 점이 문제였던것같아요..
다음 시간에도 이어서 SoundTool을 진행합니다.



<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 36회차 미션 시작합니다.

04. 배틀로얄 - 17, 18 번을 진행합니다.




이번 시간에는 저번시간에 제작한 SoundClip을 사용하는 SoundData 모델을 제작합니다.
내용 난이도는 EffectData와 크게 다르지 않지만, 다양한 옵션 등으로 인해 코드가 좀 깁니다.





위의 위치에 SoundData Script를 생성하고 작업을 시작합니다.


/// <summary>
/// 사운드 클립을 배열로 소지, 사운드 데이터를 저장하고 로드하고, 프리로딩을 가짐.
/// </summary>
public class SoundData : BaseData
{
    public SoundClip[] soundClips = new SoundClip[0];
    
    private string clipPath = "Sound/";
    private string xmlFilePath = "";
    private string xmlFileName = "soundData.xml";
    private string dataPath = "Data/soundData";
    private static string SOUND = "sound";
    private static string CLIP = "clip";
    
    public SoundData() {}
    
    public void SaveData() {
        using (XmlTextWriter xml = new XmlTextWriter(xmlFilePath + xmlFileName, System.Text.Encoding.Unicode)) {
            xml.WriteStartDocument();
            xml.WriteStartElement(SOUND);
            xml.WriteElementString("length", GetDataCount().ToString());
            xml.WriteWhitespace("\n");
            
            for (int i = 0; i < names.Length; i++) {
                SoundClip clip = soundClips[i];
                xml.WriteStartElement(CLIP);
                xml.WriteElementString("id", i.ToString());
                xml.WriteElementString("name", names[i]);
                xml.WriteElementString("loops", clip.checkTime.Length.ToString());
                xml.WriteElementString("maxvol", clip.maxVolume.ToString());
                xml.WriteElementString("pitch", clip.pitch.ToString());
                xml.WriteElementString("dopplerlevel", clip.dopplerlevel.ToString());
                xml.WriteElementString("rolloffmode", clip.rolloffMode.ToString());
                xml.WriteElementString("mindistance", clip.minDistance.ToString());
                xml.WriteElementString("maxdistance", clip.maxDistance.ToString());
                xml.WriteElementString("spartialblend", clip.spartialBlend.ToString());
                if (clip.isLoop == true) {
                    xml.WriteElementString("loop", "true");
                }
                xml.WriteElementString("clippath", clip.clipPath);
                xml.WriteElementString("clipname", clip.clipName);
                xml.WriteElementString("checktimecount", clip.checkTime.Length.ToString());
                string str = "";
                foreach (float t in clip.checkTime) {
                    str += t.ToString() + "/";
                }
                xml.WriteElementString("checktime", str);
                str = "";
                xml.WriteElementString("settimecount", clip.setTime.Length.ToString());
                foreach (float t in clip.setTime) {
                    str += t.ToString() + "/";
                }
                xml.WriteElementString("settime", str);
                xml.WriteElementString("type", clip.playType.ToString());
                
                xml.WriteEndElement(); // CLIP
            }
            
            xml.WriteEndElement(); // SOUND
            xml.WriteEndDocument();
        }
    }
   
    public void LoadData() {
        xmlFilePath = Application.dataPath + dataDirectory;
        TextAsset asset = (TextAsset)Resources.Load(dataPath, typeof(TextAsset));
        if (asset == null || asset.text == null) {
            AddData("NewSound");
            return;
        }
        
        using (XmlTextReader reader = new XmlTextReader(new StringReader(asset.text))) {
            int currentID = 0;
            while (reader.Read()) {
                if (reader.IsStartElement()) {
                    switch (reader.Name) {
                        case "length":
                            int length = int.Parse(reader.ReadString());
                            names = new string[length];
                            soundClips = new SoundClip[length];
                            break;
                        case "clip":
                        case "id":
                            currentID = int.Parse(reader.ReadString());
                            soundClips[currentID] = new SoundClip();
                            soundClips[currentID].realId = currentID;
                            break;
                        case "name":
                            names[currentID] = reader.ReadString();
                            break;
                        case "loops":
                            int count = int.Parse(reader.ReadString());
                            soundClips[currentID].checkTime = new float[count];
                            soundClips[currentID].setTime = new float[count];
                            break;
                        case "maxvol":
                            soundClips[currentID].maxVolume = float.Parse(reader.ReadString());
                            break;
                        case "pitch":
                            soundClips[currentID].pitch = float.Parse(reader.ReadString()));
                            break;
                        case "dopplerlevel":
                            soundClips[currentID].dopplerLevel = float.Parse(reader.ReadString());
                            break;
                        case "rolloffmode":
                            soundClips[currentID].rolloffMode = (AudioRolloffMode)Enum.Parse(typeof(AudioRolloffMode), reader.ReadString()));
                        case "mindistance":
                            soundClips[currentID].minDistance = float.Parse(reader.ReadString());
                            break;
                        case "maxdistance":
                            soundClips[currentID].maxDistance = float.Parse(reader.ReadString());
                            break;
                        case "spartialblend":
                            soundClips[currentID].spartialBlend = float.Parse(reader.ReadString());
                            break;
                        case "loop":
                            soundClips[currentID].isLoop = true;
                            break;
                        case "clippath":
                            soundClips[currentID].clipPath = reader.ReadString();
                            break;
                        case "clipname":
                            soundClips[currentID].clipName = reader.ReadString();
                            break;
                        case "checktimecount":
                            break;
                        case "checktime":
                            SetLoopTime(true, soundClips[currentID], reader.ReadString());
                            break;
                        case "settime":
                            SetLoopTime(false, soundClips[currentID], reader.ReadString());
                            break;
                        case "type":
                            soundClips[currentID].playType = (SoundPlayType)Enum.Parse(typeof(SoundPlayType), reader.ReadString()));
                    }
                }
            }
        }
        
        // 미리 불러와 놓기. 리소스가 많아 로딩이 느리다면 제거하라.
        foreach (SoundClip clip in soundClips) {
            clip.PreLoad();
        }
    }
    
    void SetLoopTime(bool isCheck, SoundClip clip, string timeString) {
        string[] time = timeString.Split('/');
        for (int i = 0; i < time.Length; i++) {
            if (time[i] != string.Empty) {
                if (isCheck == true)
                    clip.checkTime[i] = float.Parse(time[i]);
                else
                    clip.setTime[i] = float.Parse(time[i]);
            }
        }
    }
    
    public override int AddData(string newName) {
        if (names == null) {
            names = new string[] { newName };
            soundClips = new SoundClip[] { new SoundClip() };
        }
        else {
            names = ArrayHelper.Add(newName, names);
            soundClips = ArrayHelper.Add(new SoundClip(), soundClips);
        }
    }
    
    public override void RemoveData(int index) {
        names = ArrayHelper.Remove(index, names);
        if (names.Length == 0) names = null;
        soundClips = ArrayHelper.Remove(index, soundClips);
    }
    
    public SoundClip GetCopy(int index) {
        if (index < 0 || index >= soundClips.Length) return null;
        SoundClip clip = new SoundClip();
        SoundClip original = soundClips[index];
        clip.realId = index;
        clip.clipPath = original.clipPath;
        clip.clipName = original.clipName;
        clip.maxVolume = original.maxVolume;
        clip.pitch = original.pitch;
        clip.dopplerLevel = original.dopplerLevel;
        clip.rolloffMode = original.rolloffMode;
        clip.minDistance = original.minDistance;
        clip.maxDistance = original.maxDistance;
        clip.spartialBlend = original.spartialBlend;
        clip.isLoop = original.isLoop;
        clip.checkTime = new float[original.checkTime.Length];
        clip.setTime = new float[original.setTime.Length];
        clip.playType = original.playType;
        for (int i=0; i < clip.checkTime.Length; i++) {
            clip.checkTime[i] = original.checkTime[i];
            clip.setTime[i] = original.setTime[i];
        }
        clip.PreLoad();
        return clip;
    }
    
    public override void Copy(int index) {
        names = ArrayHelper.Add(names[index], names);
        soundClips = ArrayHelper.Add(GetCopy(index), soundClips);
    }
}




여기까지가 SoundData의 완료입니다.. ^^;;;

xml로 저장하고 불러와서 파싱하기가 주요코드인데 옵션들이 많다보니 반복적인 코드가 많고 눈이 빙글빙글 ㅎㅎ..

다음 시간에는 SoundTool을 제작합니다...





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 35회차 미션 시작합니다.

04. 배틀로얄 - 15, 16 번을 진행합니다.




BaseData::GetDataCount() 함수의 오류가 있어서 버그를 수정한 내용입니다.
위와 같이 수정하고 저번시간의 EffectTool Window를 실행해 보면 기본적인 구동이 되는 것을 확인할 수 있습니다.




ADD를 클릭하여 Item을 하나 더 추가하고.. 이름을 설정하고 Prefabs/Effects에 준비된 Effect GameObject를 마우스클릭으로 드래그하여 이팩트에 가져다 놓으면 연결되어 동작하는 것도 확인할 수 있습니다.




추가한 이후 [SAVE]를 클릭하면 effectData와 EffectList에 영향을 주게 됩니다.

우선 effectData 파일을 더블클릭하여 열어봅니다.
다음과 같이 한 줄로 구성된 xml 파일의 내용을 볼 수 있습니다.
이렇게 보면 좀 불편한대요..


 

 


이렇게 Excel 문서에서 열어서 보면 좀더 구성이 잘된 모양의 동일한 effectData xml 파일의 내용을 볼 수가 있습니다.

 



또한 EffectList 파일을 더블클릭하여 열어보면 EffectList라는 enum도 Tool에서 추가한 아이템 내용대로 업데이트된 것을 확인할 수가 있습니다.
이렇게 수행되도록 만든 코드가 CreateEnumStructure()라는 함수였고 이것이 수행된 결과인 것입니다.




이제 Manager 폴더 아래에 DataManager와 EffectManager 2개의 스크립트를 준비합니다.

우선 DataManager부터 작성을 시작합니다.

public class DataManager : MonoBehaviour
{
    private static EffectData effectData = null;
    
    void Start() {
        if (effectData == null) {
            effectData = ScriptableObject.CreateInstance<EffectData>();
            effectData.LoadData();
        }
    }
    
    public static EffectData EffectData() {
        if (effectData == null) {
            effectData = ScriptableObject.CreateInstance<EffectData>();
            effectData.LoadData();
        }
        return effectData;
    }
}


이제 EffectManager를 작성합니다.

public class EffectManager : SingletonMonoBehaviour<EffectManager>
{
    private Transform effectRoot = null;
    
    void Start() {
        effectRoot = new GameObject("EffectRoot").transform;
        effectRoot.SetParent(transform);
    }
    
    //## 핵심 코드
    public GameObject EffectOneShot(int index, Vector3 position) {
        EffectClip clip = DataManager.EffectData().GetClip(index);
        GameObject effectInstance = clip.Instantiate(position);
        effectInstance.SetActive(true);
        return effectInstance;
    }
}





SingletonMonoBehaviour의 소스는 위와 같으며 프로젝트에 기본 포함하여 배포되었습니다.

위와같이 하여 EffectTool이 끝난거라고 합니다.. 무언가 Unity에서 확인해보고 구동하는 것이 있을거라 생각했는데 아니었네요 ㅎㅎ
바로 SoundTool 제작으로 넘어갑니다..




SoundClip 스크립트를 만들고 작성을 시작합니다 ^^; SoundTool은 EffectTool과 핵심적인 기능은 동일한데 Sound의 특성상 Loop 기능이나 CheckPoint 및 FadeOut등의 효과 등의 처리가 많아서 데이터가 많기 때문에 조금 복잡하다는 것만 다르다고 보면 됩니다.


// 루프, 페이드 관련 속성, 오디오 클립 속성들.
public class SoundClip
{
    public SoundPlayType playTpe = SoundPlayType.None;
    public string clipName = string.Empty;
    public string clipPath = string.Empty;
    public float maxVolume = 1.0f;
    public bool isLoop = false;
    public float[] checkTime = new float[0];
    public float[] setTime = new float[0];
    public int realId = 0;
    
    private AudioClip clip = null;
    public int currentLoop = 0;
    public float pitch = 1.0f;
    public float dopplerLevel = 1.0f;
    public AudioRolloffMode rolloffMode = AudioRolloffMode.Logarithmic;
    public float minDistance = 10000.0f;
    public float maxDistance = 50000.0f;
    public float sparialBlend = 1.0f;
    
    public float fadeTime1 = 0.0f;
    public float fadeTime2 = 0.0f;
    public Interpolate.Function interpolate_Func;
    public bool isFadeIn = false;
    public bool isFadeOut = false;
    
    public SoundClip() { }
    public SoundClip(string clipPath, string clipName) {
        this.clipPath = clipPath;
        this.clipName = clipName;
    }
    public void PreLoad() {
        if (this.clip == null) {
            string fullPath = this.clipPath + this.clipName;
            this.clip = ResourceManager.Load(fullPath) as AudioClip;
        }
    }
    
    public void AddLoop() {
        checkTime = ArrayHelper.Add(0.0f, this.checkTime);
        setTime = ArrayHelper.Add(0.0f, this.setTime);
    }
    public void RemoveLoop(int index) {
        checkTime = ArrayHelper.Remove(index, this.checkTime);
        setTime = ArrayHelper.Remove(index, this.setTime);
    }
    public AudioClip GetClip() {
        if (clip == null) PreLoad();
        if (clip == null && clipName != string.Empty) {
            Debug.LogWarning($"Can not load audio clip Resource {clipName}");
            return null;
        }
        return clip;
    }
    public void ReleaseClip() {
        if (clip != null) clip = null;
    }
    public bool HasLoop() {
        return checkTime.Length > 0;
    }
    public void NextLoop() {
        currentLoop++;
        if (currentLoop >= checkTime.Length) currentLoop = 0;
    }
    public void CheckLoop(AudioSource source) {
        if (HasLoop() && source.time >= checkTime[currentLoop]) {
            source.time = setTime[currentLoop];
            NextLoop();
        }
    }
    
    public void FadeIn(float time, Interpolate.EaseType easeType) {
        isFadeOut = false;
        fadeTime1 = 0.0f;
        fadeTime2 = time;
        interpolate_Func = Interpolate.Ease(easeType);
        isFadeIn = true;
    }
    public void FadeOut(float time, Interpolate.EaseType easeType) {
        isFadeIn = false;
        fadeTime1 = 0.0f;
        fadeTime2 = time;
        interpolate_Func = Interpolate.Ease(easeType);
        isFadeOut = true;
    }
    /// <summary>
    /// 페이드인,아웃 효과 프로세스
    /// </summary>
    public void DoFade(float time, AudioSource audio) {
        if (isFadeIn == true) {
            fadeTime1 += time;
            audio.volume = Interpolate.Ease(interpolate_Func, 0, maxVolume, fadeTime1, fadeTime2);
            if (fadeTime1 >= fadeTime2) isFadeIn = false;
        }
        else if (isFadeOut == true) {
            fadeTime1 += time;
            audio.volume = Interpolate.Ease(interpolate_Func, maxVolume, 0 - maxVolume, fadeTime1, fadeTime2);
            if (fadeTime1 >= fadeTime2) {
                isFadeOut = false;
                audio.Stop();
            }
        }
    }
}

여기까지가 SoundClip 완성입니다. 다음시간부터는 SoundManager를 통하여 Clip을 관리하는 기능을 구현할 것입니다.
결국 SoundClip은 소리 재생인데 게임 상황상 여러가지 옵션이 많다보니 함수를 많이 구현하게 된 것입니다.


Easing functions에 대해서는 구글에서 검색한 이미지를 하나 추가합니다.






<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 34회차 미션 시작합니다.

04. 배틀로얄 - 13, 14 번을 진행합니다.




EffectTool Script 생성하여 진행합니다 ^^~


using UnityEngine;
using UnityEditor; // Editor Tool을 만들기 위해 필수
using System.Text;
using UnityObject = UnityEngine.Object;

// Clip Property를 수정하고, 읽고, 저장하는 기능.
public class EffectTool : EditorWindow
{
    //UI 그리는데 필요한 변수들.
    pubic int uiWidthXLarge = 450; // pixel 크기.
    pubic int uiWidthLarge = 300;
    pubic int uiWidthMiddle = 200;
    private int selection = 0; // 선택 index
    private Vector2 SP1 = Vector2.zero;
    private Vector2 SP2 = Vector2.zero;
}

여기까지가 기본적인 처리를 위한 준비 과정을 마친 상태이고, 이제 본격적으로 이팩트 툴 작성에 들어갑니다 ^^


 



public class EffectTool : EditorWindow
{
    // 위의 변수들..
    
    // 이팩트 클립
    private GameObject effectSource = null;
    // 이팩트 데이터
    private static EffectData effectData;
    
    [MenuItem("Tools/Effect Tool")]  // 유니티에 메뉴 생성
    static void Init() {
        effectData = ScriptableObject.CreateInstance<EffectData>();
        effectData.LoadData();
        
        EffectTool window = GetWindow<EffectTool>(false, "Effect Tool");
        window.Show();
    }
    
    private void OnGUI() {
        if (effectData == null) return;
        
        EditorGUILayout.BeginVertical();
        {
            // 상단 - add, remove, copy
            UnityObject source = effectSource;
            EditorHelper.EditorToolTopLayer(effectData, ref selection, ref source, this.uiWidthMiddle);
            effectSource = (GameObject)source;
            
            EditorGUILayout.BeginHorizontal();
            {
                // 중단 - data list
                EditorHelper.EditorToolListLayer(ref SP1, effectData, ref selection, ref source, this.uiWidthLarge);
                effectSource = (GameObject)source;
                
                // 설정 부분
                EditorGUILayout.BeginVertical();
                {
                    SP2 = EditorGUILayout.BeginScrollView(this.SP2);
                    {
                        if (effectData.GetDataCount() > 0) {
                            EditorGUILayout.BeginVertical();
                            {
                                EditorGUILayout.Separator();
                                EditorGUILayout.LabelField("ID", selection.ToString(), GUILayout.Width(uiWidthLarge));
                                effectData.names[selection] = EditorGUILayout.TextField("이름.", effectData.names[selection], GUILayout.Width(uiWidthXLarge));
                                effectData.effectClips[selection].effectType = (EffectType)EditorGUILayout.EnumPopup("이팩트 타입.", effectData.effectClips[selection].effectType, GUILayout.Width(uiWidthLarge));
                                EditorGUILayout.Separator();
                                if (effectSource == null && effectData.effectClips[selection].effectName != string.Empty) {
                                    effectData.effectClips[selection].PreLoad();
                                    effectSource = Resources.Load(effectData.effectClips[selection].effectPath + effectData.effectClips[selection].effectName) as GameObject;
                                }
                                effectSource = (GameObject)EditorGUILayout.ObjectField("이팩트", this.effectSource, typeof(GameObject), false, GUILayout.Width(uiWidthXLarge));
                                if (effectSource != null) {
                                    effectData.effectClips[selection].effectPath = EditorHelper.GetPath(this.effectSource);
                                    effectData.effectClips[selection].effectName = effectSource.name;
                                }
                                else {
                                    effectData.effectClips[selection].effectPath = string.Empty;
                                    effectData.effectClips[selection].effectName = string.Empty;
                                    effectSource = null;
                                }
                                EditorGUILayout.Separator();
                            }
                            EditorGUILayout.EndVertical();
                        }
                    }
                    EditorGUILayout.EndScrollView();
                }
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndHorizontal();
        }
        EditorGUILayout.EndVertical();
        
        EditorGUILayout.Separator();
       
        // 하단
        EditorGUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("Reload Settings")) {
                effectData = CreateInstance<EffectData>();
                effectData.LoadData();
                selection = 0;
                this.effectSource = null;
            }
            if (GUILayout.Button("Save")) {
                EffectTool.effectData.SaveData();
                CreateEnumStructure();
                AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            }
        }
        EditorGUILayout.EndHorizontal();
    }
    
    public void CreateEnumStructure() {
        string enumName = "EffectList";
        StringBuilder builder = new StringBuilder();
        builder.AppendLine();
        for (int i = 0; i < effectData.names.Length; i++) {
            if (effectData.names[i] != string.Empty) {
                builder.AppendLine("   " + effectData.names[i] + " = " + i + ",");
            }
        }
        EditorHelper.CreateEnumStructure(enumName, builder);
    }
}


또 EffectData가 VisualStudio에서 오류를 발생하네요.. 전체 컴파일이라거나 namespace 등의 문제가 아닐까 싶긴합니다만..
관련 변수, 함수들 인텔리전스가 지원되지 않으니 강의하면서 답답하실 듯 하네요.. 미리 확인이 되지 않았던건가 하는 아쉬움은 남네요.
어여 해결이 되어서 뒤의 내용들에서는 수월하게 진행될 수 있기를 바랍니다.




드디어 위에까지 작업을 하면 위와 같이 Tools - Effect Tool 이라는 메뉴가 생성된 것을 볼 수가 있습니다..


 



그리고 클릭을 해보면 와우~ Vertical, Horizontal 함수를 열심히 사용하여 만든 "Effect Tool" 창이 나타나고, 지금까지 작업한대로 버튼들과 리스트컨트롤이 하나 붙어서 보이는 것을 확인할 수 있습니다.

Unity UI 제작을 일일이 Typing으로 하니 소스보기가 좀 복잡하네요 ㅎㅎ. 좀더 복잡한 UI를 가지는 사운드 툴은 확실히 정신이 없을 듯하네요..

Unity UI 제작을 지원하는 Addon 등이 없나 궁금해지네요.

 

궁금해서 검색해보니 Unity UI Addons를 누가 만들어 배포하고 있긴 하네요 ^^. 물론 안정성이나 검증되지 않아 사용하지 않으셨는지는 모르겠습니다. 나중에 시간이 되면 사용해 봐야겠습니다.

 


아직 버그들이 있기에 좀더 수정 작업을 진행하게 되고, 다음 시간에는 데이터 매니저와 이팩트 매니저를 만들어서 전체 EffectTool이 구동되는 것을 확인하고, 바로 이어서 사운드툴 제작으로 들어가겠습니다 ^^~





<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 33회차 미션 시작합니다.

04. 배틀로얄 - 11, 12 번을 진행합니다.


저번 시간에 이어 Save, Add, Remove, Copy 등의 구현을 시작합니다.




아래 진행하는 코드 중 ArrayHelper라는 클래스는 제공되는 프로젝트에 포함되어 있는 코드를 사용합니다.
Array 기능을 좀더 쉽게 사용할 수 있는 기능을 가진 함수 모음 클래스라고 생각하면 됩니다.
크게 중요하지 않은 내용이므로 제공된 소스를 사용만 하면 됩니다. Tool용 ArrayHelper 입니다.



public class EffectData : DataBase
{
    // 저번시간코드들..
    
    public void SaveData() {
        using (XmlTextWriter xml = new XmlTextWriter(xmlFilePath + xmlFileName, System.Text.Encoding.Unicode) {
            xml.WriteStartDocument();
            xml.WriteStartElement(EFFECT); // EFFECT Key
            xml.WriteElementString("length", GetDataCount().ToString());
            for (int i = 0; i < this.names.Length; i++) {
                EffectClip clip = this.effectClips[i];
                xml.WriteStartElement(CLIP);  // CLIP Key
                xml.WriteElementString("id", i.ToString());
                xml.WriteElementString("name", this.names[i]);
                xml.WriteElementString("effectType", clip.effectType.ToString());
                xml.WriteElementString("effectPath", clip.effectPath);
                xml.WriteElementString("effectName", clip.effectName);
                xml.WriteEndElement(); // CLIP End
            }
            xml.WriteEndElement(); // EFFECT End
            xml.WriteEndDocument();
        }
    }
   
    public override int AddData(string newName) {
        if (this.names == null) {
            this.names = new string[] { name };
            this.effectClips = new EffectClip[] { new EffectClip() };
        }
        else {
            this.names = ArrayHelper.Add(name, this.names);
            this.effectClips = ArrayHelper.Add(new EffectClip(), this.effectClips);
        }
        return GetDataCount();
    }
    
    public override void RemoveData(int index) {
        this.names = ArrayHelper.Remove(index, this.names);
        if (this.names.Length == 0) this.names = null;
        this.effectClips = ArrayHelper.Remove(index, this.effectClips);
    }
    
    public void ClearData() { // Bonus function
        foreach (EffectClip clip in this.effectClips) {
            clip.ReleaseEffect();
        }
        this.effectClips = null;
        this.names = null;
    }
    
    public EffectClip GetCopy(int index) {
        if (index < 0 || index >= this.effectClips.Length) return null;
        EffectClip original = this.effectClips[index];
        EffectClip clip = new EffectClip();
        clip.effectFullPath = original.effectFullPath;
        clip.effectName = original.effectName;
        clip.effectType = original.effectType;
        clip.effectPath = original.effectPath;
        clip.realId = this.effectClips.Length;
        return clip;
    }
    
    /// <summary>
    /// 원하는 인덱스를 프리로딩해서 찾아준다
    /// </summary>
    public EffectClip GetClip(int index) {
        if (index < 0 || index >= this.effectClips.Length) return null;
        effectClips[index].PreLoad();
        return effectClips[index];
    }
    
    public override void Copy(int index) {
        this.names = ArrayHelper.Add(this.names[index], this.names);
        this.effectClips = ArrayHelper.Add(GetCopy(index), this.effectClips);
    }
}


여기까지가 EffectData 에 대한 처리 마무리였고 이제부터 본격적인 Tool 개발에 들어갑니다 ^^~





공통 툴 레이어 작성입니다.

본격적인 EffectTool 제작 전에, 공통 툴을 만들어두면 다른 툴을 만들때에도 재사용 가능하기 때문에 제작해두는 것이 좋습니다.




EditorHelper 클래스입니다.

ArrayHelper와 비슷하게 Tool 개발을 용이하게 해주는 함수들의 모음이며 프로젝트에 포함되어 배포되어 있습니다.

다음의 2가지 함수는 구현되어 있고, 나머지 함수들을 추가 구현합니다.

+ GetPath() - Resource를 전달하면 Resource의 경로를 리턴해주는 함수
+ CreateEnumStructure() - Enum 객체를 만들어주는 함수

 


public class EditorHelper
{
    public static string GetPath(UnityEngine.Object p_clip) {}
    public static void CreateEnumStructure(string enumName, StringBuilder data) {}
    
    // Tool UI 상단
    public static void EditorToolTopLayer(BaseData data, ref int selection, ref UnityObject source, int uiWidth) {
        EditorGUILayout.BeginHorizontal();
        {
            if (GUILayout.Button("ADD", GUILayout.Width(uiWidth))) {
                data.AddData("New Data");
                selection = data.GetDataCount() - 1; // 최종 Item 선택
                source = null;
            }
            if (GUILayout.Button("COPY", GUILayout.Width(uiWidth))) {
                data.Copy(selection);
                source = null;
                selection = data.GetDataCount() - 1;
            }
            if (data.GetDataCount() > 1) {
                if (GUILayout.Button("REMOVE", GUILayout.Width(uiWidth))) {
                    source = null;
                    data.RemoveData(selection);
                }
            }
            
            if (selection > data.GetDataCount() - 1) { // out of range
                selection = data.GetDataCount() - 1;
            }
        }
        EditorGUILayout.EndHorizontal();
    }
    
    // Tool UI 리스트
    public static void EditorToolListLayer(ref Vector2 ScrollPosition, BaseData data, ref int selection, ref UnityObject source, int uiWidth) {
        EditorGUILayout.BeginVertical(GUILayout.Width(uiWidth);
        {
            EditorGUILayout.Separator();
            EditorGUILayout.BeginVertical("box");
            {
                ScrollPosition = EditorGUILayout.BeginScrollView(ScrollPosition);
                {
                    if (data.GetDataCount() > 0) {
                        int lastSelection = selection;
                        selection = GUILayout.SelectionGrid(selection, data.GetNameList(true), 1);
                        if (lastSelection != selection) { // 선택이 바뀌었으면
                            source = null;
                        }
                    }
                }
                EditorGUILayout.EndScrollView();
            }
            EditorGUILayout.EndVertical();
        }
        EditorGUILayout.EndVertical();
    }
}


다음시간에 공통 툴 레이어 완성 및 본격적인 EffectTool 만들기를 시작합니다 ^^~




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 32회차 미션 시작합니다.

04. 배틀로얄 - 09, 10 번을 진행합니다.


저번에 이어서 이번에는 EffectData 클래스를 구현합니다.




코드 내용중 clipPath는 위에서 보듯이 Unity Project에서 "9.ResourcesData/Resources/Prefabs/Effects" 폴더를 가리킵니다.




마찬가지로 코드 내용중 dataPath는 위에서 보이는 Unity Project에서 "9.ResourcesData/Resources/Data" 폴더를 가리킵니다.



using System.Xml; // XML 사용.
using System.IO; // 읽기 쓰기.
/// <summary>
/// 이팩트 클립 리스트와 이팩트 파일 이름과 경로를 가지고 있으며 파일을 읽고 쓰는 기능을 가지고 있다.
/// </summary>
public class EffectData : BaseData
{
    // Array는 실수로 무한정 커질 수 있는 문제를 가지고 있어서 한정된 자원이라는 의미와 크기 명확성을 위해 배열을 사용.
    public EffectClip[] effectClips = new EffectClip[0];
    
    public string clipPath = "Effects/";
    private string xmlFilePath = ""; // Path, File 분리이유는 경로만 바꾸어 파일관리, 버전관리가 용이하며, 특정 Asset 폴더로 관리하는 등 유연하기 때문.
    private string xmlFileName = "effectData.xml";
    private string dataPath = "Data/effectData";
    //XML 구분자.
    private const string EFFECT = "effect"; // 저장 KEY
    private const string CLIP = "clip"; // 저장 KEY
    
    private EffectData() { }
    
    // 읽어오고 저장하고, 데이터를 삭제하고, 특정 클립을 얻어오고, 복사하는 기능.
    public void LoadData() {
        Debug.Log($"xmlFilePath = {Application.dataPath} + {dataDirectory}");
        this.xmlFilePath = Application.dataPath + dataDirectory; // Application.dataPath = Unity Assets Folder
        TextAsset asset = (TextAsset)ResourceManager.Load(dataPath);
        if (asset == null || asset.text == null) { // 하나도 없는 경우
            this.AddData("new Effect");
            return;
        }
        
        using (XmlTextReader reader = new XmlTextReader(new StringReader(asset.text))) {
            int currentID = 0;
            while (reader.Read()) {
                if (reader.IsStartElement()) {
                    switch (reader.Name) {
                        case "length":
                            int length = int.Parse(reader.ReadString());
                            this.names = new string[length];
                            this.effectClips = new EffectClip[length];
                            break;
                        case "id":
                            currentID = int.Parse(reader.ReadString());
                            this.effectClips[currentID] = new EffectClip();
                            this.effectClips[currentID].realId = currentID;
                            break;
                        case "name":
                            this.names[currentID] = reader.ReadString();
                            break;
                        case "effectType":
                            this.effectClips[currentID].effectType = (EffectType)Enum.Parse(typeof(EffectType), reader.ReadString());
                            break;
                        case "effectName":
                            this.effectClips[currentID].effectName = reader.ReadString();
                            break;
                        case "effectPath":
                            this.effectClips[currentID].effectPath = reader.ReadString();
                            break;
                    }
                }
            }
        }
    }
}


초반 개발시에는 데이터 파일 저장 포맷이 지속적으로 바뀌게 될텐데 이때부터 json을 사용해도 되지만 json을 사용하게 되는 경우 무언가 잘못되어 꼬이게 되는 경우 저장 구조가 복잡하여 해당 문제점을 찾기가 어렵다는 단점이 있습니다.
그래서 초반에는 xml과 같은 명확하고 찾기 쉬운 데이터 파일 구조를 사용하다가 어느 정도 안정화되면 json으로 변경하는 것도 좋은 방법입니다.

사실 실무에서는 자체 개발된 데이터 파일 구조를 많이 사용한다고 합니다.




위와 같이 $를 붙여서 사용하면 좀더 편하게 문자열 구조를 사용할 수 있습니다.
지원되지 않았을 때는 아래 문장처럼 +를 마구마구 써서 연결해야 했던 불편함이 컸었지요.

C#은 정말 개발자 편의성을 정말 많이 고려한 것 같습니다.

 


그런데 왜 1.2x를 없앤거죠?? 1x 1.5x 2x만 나오는... 갑자기 오늘부터 지원을 안하네요.. 라고 하고 있는데 잠시 그랬던거 같네요 ^^;;;
다시 불러와서 재생하니 정상적으로 나오네요.. 서버 작업중이신건가 ㅎㅎ
헉.. 그런가보네요.. 갑자기 저화질로 나오고 서버 일시오류라고 뜨며 멈추고 튕기기도 하네요 ㅠ.,ㅜ;; 무어지.. 뜨합..
어여 안정화시켜주세요..




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 31회차 미션 시작합니다.

04. 배틀로얄 - 07, 08 번을 진행합니다.





일단 Assets/1.Scripts 폴더에 대한 간단한 설명입니다.

+ Common - 공통적인 스크립트들
+ GameData - GameAsset에 쓰일 데이터 스크립트들
+ Helper - 에디터, 툴 Helper 스크립트들
+ Tool - 툴 스크립트들
+ Util - 시간계산, 위치계산 등의 유틸 스크립트들

+ Manager 폴더를 만들고, C# 스크립트 ResourceManager를 추가합니다.


using UnityEngine;
using UnityObject = UnityEngine.Object; // Object를 어디것인지 명확히 하여 사용.
/// <summary>
/// Resources.Load를 래핑하는 클래스
/// 나중엔 어셋번들로 변경됨.
/// </summary>
public class ResourceManager
{
    // ResourceManager 클래스를 Singletone 패턴으로 구현해도 되지만, 간단한 클래스는 그냥 함수를 static으로 해서 지원해도 편하다.
    public static UnityObject Load(string path) {
        // 지금은 리소스 로드지만 추후엔 어셋 로드로 변경됨.
        return Resources.Load(path);
    }
    
    public static GameObject LoadAndInstantiate(string path) {
        UnityObject source = Load(path);
        if (source == null) return null;
        return GameObject.Instantiate(source) as GameObject;
    }
}





이팩트 툴 제작을 위한 이팩트 데이터 모델을 먼저 제작합니다.
여기 내용들은 게임 장르와 기획에 따라 엄청나게 바뀌게 됩니다. 이 툴을 사용하게 되는 분들은 기획자나 밸런서분들이 될 것입니다. 그래서 요구사항도 많아지게 될 것입니다.

불필요한듯하고 굉장히 손도 많이 가는 툴을 왜 만드느냐고 의문을 가지는 개발자들이 많은 듯하네요..
저는 무조건 찬성입니다. 무조건 Tool을 개발해야 합니다. 게임뿐만이 아니라 어떠한 프로그램을 만들더라도 마찬가지라고 생각합니다. 이러한 AddOn Tool이든 Helper Tool이든 마찬가지라고 생각합니다.

Tool을 만들다 보면 3번의 고비가 온다고 합니다. ㅋㅋㅋㅋㅋㅋㅋㅋ.
사운드 툴 만들 때, 캐릭터 만들 때, 그리고 애니메이션 만들 때라고 합니다. ㅎㅎ 잘 넘기고 이겨내면 멋진 게임을 개발해 낼 수가 있게 되는 것이겠지요..


<Effect Clip>
+ Prefab - 터지는 효과, 피격 효과, 터지는 Mesh 등
+ 생명주기 - 몇초후에 사라져라 또는 화면 밖으로 넘어가면 사라져라 등
+ 위치 - 캐릭터의 손에 붙는 이팩트인가, 피격이팩트.
+ 사운드 등등..


<Effect Data>
+ Effect Clip들의 모음





EffectClip과 EffectData 스크립트를 추가합니다.

Effect Tool은 그래도 간단한 편입니다. Sound Tool로 가면 많이 복잡해진다고 합니다 ^^; 기대도 되고 긴장도 되네요.



/// <summary>
/// 이팩트 프리팹과 경로와 타입 등의 속성 데이터를 가지고 있게 되며
/// 프리팹 PreLoading 기능을 갖고 있고 - 풀링을 위한 기능이기도 합니다.
/// 이팩트 인스턴스 기능도 갖고 있으며 - 풀링과 연계해서 사용하기도 합니다.
/// </summary>
public class EffectClip
{
    public int realId = 0; // 추후 속성은 같지만 다른 이팩트 클립이 있을 수 있어서 분별용.
    
    public EffectType effectType = EffectType.NORMAL;
    public GameObject effectPrefab = null;
    public string effectName = string.Empty;
    public string effectPath = string.Empty;
    public string effectFullPath = string.Empty; // effectPath + effectName
    
    public EffectClip() { }
    
    public void PreLoad() {
        this.effectFullPath = effectPath + effectName;
        if (this.effectFullPath != string.mpty && this.effectPrefab == null) { // prefab 로딩이 되어있다면 다시 할 필요 없으므로
            this.effectPrefab = ResourceManager.Load(effectFullPath) as GameObject;
            //this.effectPrefab = (GameObject)ResourceManager.Load(effectFullPath); // 강제형변환을 앞에 넣어도 되지만 중요하지 않은 구문이 앞으로 오기 때문에 "as" 구문이 유용한 경우가 많다.
        }
    }
    
    public void ReleaseEffect() {
        if (this.effectPrefab != null)
            this.effectPrefab = null; // Destroy하지 않아도 GabageCollector가 처리할 것이다.
    }

    /// <summary>
    /// 원하는 위치에 내가 원하는 이팩트를 인스턴스합니다.
    /// </summary>
    public GameObject Instantiate(Vector3 Pos) {
        if (this.PreLoad() != null) {
            GameObject effect = GameObject.Instantiate(effectPrefab, Pos, Quaternion.identity);
            return effect;
        }
        
        return null;
    }
}





EffectType에서 "Ctrl + Click"을 하면 정의로 이동을 하게 됩니다.
GlobalDefine 스크립트는 위와 같은 내용을 가지고 있습니다.

 

걱정도 많으시고 받아들이는 사람에 대한 큰 배려심이기도 하셔서 반복적인 걱정 얘기를 많이하셔서 아직까지는 내용이 짧습니다. ㅎㅎ 강의 진행하시면서 개발 시간때문에라도 많이 빨라지지 않을까 싶습니다.

 

일단 화면도 잘 보이고.. VisualStudio 검정 배경에 글씨들도 잘 보입니다.

온라인 강의라 놓친 부분들은 다시 볼 수 있기 때문에 너무 걱정 안하시고 좋은 얘기와 내용 많이 해주시면 좋겠습니다.




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 30회차 미션 시작합니다.

04. 배틀로얄 - 05, 06 번을 진행합니다.




"Assets/1.Scripts/GameData" 폴더 내에 C# Script를 "BaseData"로 하나 생성합니다.

ScriptableObject는 클래스 인스턴스와는 별도로 대량의 데이터를 저장하는데 사용할 수 있는 데이터 컨테이너입니다.

json을 실무에서 많이 사용하고 가장 좋지만, 이후 설명에서도 사용할 것이기 때문에 여기서는 비교 스터디 목적으로 xml 기반으로 구동되는 클래스를 작성합니다.

LinQ 구문도 가능하면 꼭 필요한 부분에서만 사용하겠다고 하시네요. 기존 C++ 개발자분들을 배려하시는 듯 합니다.



/// <summary>
/// data의 기본 클래스입니다.
/// 공통적인 데이트를 가지고 있게 되는데, 이름만 현재 갖고 있다.
/// 데이터의 갯수와 이름의 목록 리스트를 얻을 수 있다.
/// </summary>
public class BaseData : ScriptableObject
{
    public const string dataDirectory = "/9.ResourcesData/Resources/Data/";
    public string[] names = null; // 데이터를 읽기 전이라는 의미로 null 처리.
    
    public BaseData() { }
    
    public int GetDataCount() {
        int retValue = 0;
        if (this.names != null)
            retValue = this.name.Length; // Length는 Property, Count()는 확장 메서드. 실제 사용상 차이 있음.
        return retValue;
    }
    
    /// <summary>
    /// 툴에 출력하기 위한 이름 목록을 만들어주는 함수.
    /// </summary>
    public string[] GetNameList(bool showID, string filterWord = "") {
        string[] retList = new string[0];
        if (this.names == null) return retList;
        
        retList = new string[this.names.Length];
        for(int i = 0; i < this.names.Length; i++) {
            if (filterWord != "") {
                if (names[i].toLower().Contains(filterWorld.ToLower()) == false) {
                    continue;
                }
            }
            if (showID) {
                retList[i] = i.ToString() + " : " + this.names[i]; // index를 붙여서 보여주기.
            }
            else {
                retList[i] = this.names[i];
            }
        }
        return retList;
    }
    
    public virtual int AddData(string newName) {
        return GetDataCount();
    }
    
    public virtual void RemoveData(int index) {
    }
    
    public virtual void Copy(int index) {
    }
}





///를 입력하여 summary를 입력해줌으로써 다른 개발자가 해당 클래스를 사용하려고 할 때 많은 도움이 되기 때문에 현업에서는 사용하는 경우가 많습니다. 지금은 클래스가 몇 개 되지 않아 큰 도움이 될 것같이 보이진 않아도, 나중에 프로젝트가 커져서 클래스, 함수가 많아지게 되면 도움이 될 것입니다.

대신 summary를 입력해놓고 열심히 개발하다보니 클래스, 함수의 내용이 많이 변하였는데 summary 내용이 업데이트되지 않으면 오히려 혼선만 발생할 수 있습니다. 그래서 그럴거면 아예 달지 말자는 얘기도 있다고 합니다만...

너무 모든 곳에 다 설명을 달려고 하지 말고 클래스와 주요 함수 정도에는 달아주는 것이 좋습니다. 대신 함수를 업데이트하게 된다면 꼭 summary 내용을 확인하여 위배된다면 summary도 업데이트 해주는 것을 잊지말아야겠습니다.

summary는 협업에 있어서는 매너라고 생각하면 될 것 같습니다.



소스코드내 dataDirectory는 위와 같이 Unity Project에 있는 "Data" 폴더를 가리키게 됩니다.


 


"Edit - Preferences.." 에서 "External Tools"에서 "External Script Editor"가 설정되어 있어야 Unity와 Editor가 정상적으로 연동되고 컴파일됩니다.

실무에서는 "Rider(유료)"를 많이 사용하는데 여기서는 "Visual Studio 2019 (Community)" 버전을 사용하여 진행합니다.

 

 

여기까지 기반이 되는 BaseData 클래스를 제작하였고, 다음 시간에는 실제 Tool 제작에 들어가겠네요 ^^~

 




<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

+ Recent posts