지난 번에는 대충 어떤 식으로 콘솔을 입력하고 머드 게임을 구성하는지 테스트 해보던 단계라 기능이 분리되어 있지 않았다. 그래서 이번엔 클래스를 조금 더 잘게 분리한 뒤 몇 가지 기능을 추가하였다.

 

먼저 GameCore의 코드이다.

더보기
더보기
using C_Study;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace C_Study
{
    public enum LevelType
    {
        Start,
        Setting,
        Menu,
        Play,
    }

    public class GameCore
    {

        public void Start()
        {
            _player = new Player();
            _levelType = LevelType.Start;

            _levels = new Dictionary<LevelType, GameLevel>();

            _levels.Add(LevelType.Start, new StartLevel(this));
            _levels.Add(LevelType.Setting, new SettingLevel(this));
            _levels.Add(LevelType.Menu, new MenuLevel(this));
        }

        public void Update()
        {
            while (_player != null)
            {
                _levels[_levelType].Update();
            }
        }

        public void End()
        {

        }

        public LevelType CurrentLevel
        {
            set { _levelType = value; }
        }

        public Player CurrentPlayer
        {
            get { return _player; }
        }


        private Player _player;
        private LevelType _levelType;

        private Dictionary<LevelType, GameLevel> _levels;
    }
}

지난번엔 GameCore 내부에 모든 기능을 구현해서 업데이트 함수를 돌리고 있었지만, Level관련 인스턴스를 따로 만들어서 Dictionary 자료구조에 삽입하였고 GameCore는 update함수만 호출하도록 하였다.

 

각 레벨은 생성자에서 GameCore를 인자로 받으면, 해당 Core를 자료구조에 보유하도록 하였다.

Level에서 Core의 옵션을 변경하면서 레벨을 왔다갔다 하기 위해서이다.

 

모든 Level클래스는 GameLevel이라는 추상 클래스를 상속받도록 구현하였다.

 

아래는 GameLevel 클래스이다.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public abstract class GameLevel
    {
        protected GameLevel() { }

        public abstract void Update();

        protected GameCore _parentCore;

    }
}

아래는 이를 상속받은 클래스들이다.

 

시작 화면을 담당하는 StartLevel.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class StartLevel : GameLevel
    {
        public StartLevel(GameCore parrentCore)
        {
            _parentCore = parrentCore;
        }

        public override void Update()
        {
            Console.Clear();

            Console.WriteLine("*************************************************");
            Console.WriteLine("*************************************************");

            Console.Write("*****************");
            DevFunctions.WriteColored("전사의 모험 RPG", ConsoleColor.Yellow);
            Console.WriteLine("*****************");

            Console.Write("******************");
            DevFunctions.WriteColored("제작자 오의현", ConsoleColor.Yellow);
            Console.WriteLine("******************");

            Console.WriteLine("*************************************************");
            Console.WriteLine("*************************************************");
            Console.WriteLine();

            Console.WriteLine("모험을 시작하시겠습니까?");

            DevFunctions.WriteLineColored("1 : YES", ConsoleColor.Blue);
            DevFunctions.WriteLineColored("2 : NO", ConsoleColor.Red);

            string input = Console.ReadLine();

            if (DevFunctions.IsNumeric(input) == false)
            {
                return;
            }

            int toInt = int.Parse(input);

            switch (toInt)
            {
                case 1:
                    _parentCore.CurrentLevel = LevelType.Setting;
                    break;
                case 2:
                    Environment.Exit(0);
                    break;
            }
        }

    }
}

 

캐릭터 선택을 담당하는 SettingLevel

(아직은 무기 선택만 있지만, 나중에는 뭐 여러가지 추가할 수도 있다. 안할수도 있고..)

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class SettingLevel : GameLevel
    {

        public SettingLevel(GameCore parrentCore)
        {
            _parentCore = parrentCore;
        }

        public override void Update()
        {
            WeaponSelect();
            WeaponFixOrReselect();
        }

        private void WeaponSelect()
        {
            while (true)
            {
                Console.Clear();

                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("----------------캐릭터 설정 단계-----------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine();

                DevFunctions.WriteLineColored("무기 선택 단계입니다.", ConsoleColor.Cyan);
                Console.WriteLine();

                Console.WriteLine("검은 기본 공격력이 낮지만 크리티컬 확률이 가장 높습니다.");
                Console.WriteLine("공격시 일정 확률로 출혈을 발생시키기도 합니다.");
                Console.WriteLine();

                Console.WriteLine("창은 적당한 공격력과 크리티컬 확률을 보유하고 있습니다.");
                Console.WriteLine("피격시 일정 확률로 적의 공격을 회피합니다.");
                Console.WriteLine();

                Console.WriteLine("망치는 높은 공격력을 보유하고 있지만 크리티컬 확률이 낮습니다.");
                Console.WriteLine("공격시 일정 확률로 적을 기절 상태로 만듭니다.");
                Console.WriteLine();

                Console.WriteLine("어떤 무기를 선택하시겠습니까?");
                DevFunctions.WriteColored("1.오래된 검 ", ConsoleColor.Blue);
                DevFunctions.WriteColored("2.오래된 창 ", ConsoleColor.Red);
                DevFunctions.WriteColored("3.오래된 망치", ConsoleColor.Green);
                Console.WriteLine();

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                if (input.Length > 1)
                {
                    continue;
                }

                int toInt = int.Parse(input);

                switch (toInt)
                {
                    case 1:
                        _parentCore.CurrentPlayer.EquipedWeapon = new OldSword();
                        break;
                    case 2:
                        _parentCore.CurrentPlayer.EquipedWeapon = new OldSpear();
                        break;
                    case 3:
                        _parentCore.CurrentPlayer.EquipedWeapon = new OldHammer();
                        break;
                }

                break;
            }
        }

        private void WeaponFixOrReselect()
        {
            while (true)
            {
                Console.Clear();

                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("----------------캐릭터 설정 단계-----------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine("-------------------------------------------------");
                Console.WriteLine();

                Console.Write("선택하신 무기는 ");
                DevFunctions.WriteColored(_parentCore.CurrentPlayer.EquipedWeapon.WPName, ConsoleColor.Cyan);
                Console.Write("입니다.");
                Console.WriteLine();

                Console.WriteLine("선택하신 무기로 게임을 시작하시겠습니까?");
                DevFunctions.WriteLineColored("1. 네", ConsoleColor.Blue);
                DevFunctions.WriteLineColored("2. 다시 선택할래요", ConsoleColor.Red);

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                if (input.Length > 1)
                {
                    continue;
                }

                int toInt = int.Parse(input);

                if(toInt != 1 && toInt != 2)
                {
                    continue;
                }

                switch (toInt)
                {
                    case 1:
                        _parentCore.CurrentLevel = LevelType.Menu;
                        break;
                    case 2:
                        _parentCore.CurrentLevel = LevelType.Setting;
                        break;
                }

                break;
            }
        }
    }
}

아래는 인벤토리 보기, 캐릭터 스탯 보기, 전투하기 등의 메뉴를 선택하는 MenuLevel이다.

_parentCore에서 멤버변수를 사용하는 코드가 너무 길어서, 별도의 get를 따로 만들어야 할 듯 하다.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class MenuLevel : GameLevel
    {
        private delegate void UpdateFunc();
        private delegate void SelectFunc();

        public MenuLevel(GameCore parrentCore)
        {
            _parentCore = parrentCore;

            selectfunc = new SelectFunc(SelectMenu);
        }

        public override void Update()
        {
            Console.Clear();

            if (updatefunc != null)
            {
                updatefunc();
            }

            if (selectfunc != null)
            {
                selectfunc();
            }
        }

        private void WriteStatus()
        {
            DevFunctions.WriteLineColored("착용중인 무기 정보", ConsoleColor.DarkCyan);

            Console.WriteLine("이름 : {0}  ", _parentCore.CurrentPlayer.EquipedWeapon.WPName);
            Console.WriteLine("공격력 : {0}  ", _parentCore.CurrentPlayer.EquipedWeapon.AttPower);
            Console.WriteLine("크리티컬 확률 : {0}%  ", _parentCore.CurrentPlayer.EquipedWeapon.CriticalProb);
            Console.WriteLine();

            DevFunctions.WriteLineColored("플레이어 스탯 정보", ConsoleColor.DarkCyan);
            Console.WriteLine("레벨 : {0}  ", _parentCore.CurrentPlayer.Level);
            Console.WriteLine("잔여 스탯 포인트 : {0}  ", _parentCore.CurrentPlayer.StatPoint);
            Console.WriteLine("공격력 : {0} ({1} + {2})  ", _parentCore.CurrentPlayer.AttPower + _parentCore.CurrentPlayer.EquipedWeapon.AttPower, _parentCore.CurrentPlayer.AttPower, _parentCore.CurrentPlayer.EquipedWeapon.AttPower);
            Console.WriteLine("크리티컬 확률 : {0} ({1} + {2})%  ", _parentCore.CurrentPlayer.CriticalProb + _parentCore.CurrentPlayer.EquipedWeapon.CriticalProb, _parentCore.CurrentPlayer.CriticalProb, _parentCore.CurrentPlayer.EquipedWeapon.CriticalProb);
            Console.WriteLine("크리티컬 데미지 : {0}%  ", _parentCore.CurrentPlayer.CriticalPower);
            Console.Write("무기 숙련도 : {0}  ", _parentCore.CurrentPlayer.WeaponSkilled);
            DevFunctions.WriteLineColored("*무기 숙련도가 높을수록 출혈, 회피, 기절 확률이 증가합니다.", ConsoleColor.DarkRed);
            Console.WriteLine("방어력 : {0}  ", _parentCore.CurrentPlayer.DefPower);
            Console.WriteLine("체력 : {0} / {1}  ", _parentCore.CurrentPlayer.CurrentHP, _parentCore.CurrentPlayer.MaxHP);
            Console.WriteLine("경험치 : {0} / {1}  ", _parentCore.CurrentPlayer.CurrentEXP, _parentCore.CurrentPlayer.MaxEXP);
            Console.WriteLine();
        }

        private void WriteInventory()
        {
            DevFunctions.WriteLineColored("보유 아이템 정보", ConsoleColor.DarkCyan);
            Console.WriteLine("아이템 개수 : {0}  ", _parentCore.CurrentPlayer.Inventory.Count);
            Console.WriteLine();
        }

        private void SelectMenu()
        {
            while (true)
            {
                Console.WriteLine("1. 캐릭터 정보를 확인한다.");
                Console.WriteLine("2. 인벤토리를 확인한다.");
                Console.WriteLine("3. 전투를 시작한다.");
                Console.WriteLine("4. 게임을 종료한다.");

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                if (input.Length > 1)
                {
                    continue;
                }

                int toInt = int.Parse(input);

                switch (toInt)
                {
                    case 1:
                        updatefunc = new UpdateFunc(WriteStatus);
                        break;
                    case 2:
                        updatefunc = new UpdateFunc(WriteInventory);
                        break;
                    case 3:
                        updatefunc = null;
                        selectfunc = new SelectFunc(SelectMonster);
                        break;
                }

                break;
            }
        }

        private void SelectMonster()
        {
            while (true)
            {
                DevFunctions.WriteLineColored("전투하고 싶은 몬스터를 선택하세요.", ConsoleColor.DarkCyan);
                Console.WriteLine();

                Console.WriteLine("1. 고블린 (적정 레벨 : 1)");
                Console.WriteLine("2. 오우거 (적정 레벨 : 5)");
                Console.WriteLine("3. 드래곤 (적정 레벨 : 10)");
                Console.WriteLine("4. 이전으로 돌아간다.");

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                if (input.Length > 1)
                {
                    continue;
                }

                int toInt = int.Parse(input);

                switch (toInt)
                {
                    case 4:
                        selectfunc = new SelectFunc(SelectMenu);
                        break;
                }
                break;
            }
        }

        private UpdateFunc updatefunc;
        private SelectFunc selectfunc;
    }
}

 

레벨은 일단 여기까지 구현하였고, Player 클래스 내부에도 몇가지 속성을 추가하였다.

아래는 Player 클래스이다.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class Player
    {
        public Player()
        {
            _attPower = 10;
            _defPower = 10;

            _maxHP = 100;
            _currentHP = 100;

            _maxEXP = 100;
            _currentEXP = 0;

            _criticalPower = 150.0f;
            _criticalProb = 5.0f;

            _level = 1;
            _statPoint = 0;

            _weaponSkilled = 1;

            _inventory = new List<Item>();
        }

        public Weapon EquipedWeapon
        {
            get { return _equipedWeapon; }
            set { _equipedWeapon = value; }
        }

        public int AttPower
        {
            get { return _attPower; }
        }
        public int DefPower
        {
            get { return _defPower; }
        }

        public int Level
        {
            get { return _level; }
        }

        public int StatPoint
        {
            get { return _statPoint; }
        }

        public int WeaponSkilled
        {
            get { return _weaponSkilled; }
        }

        public int MaxHP
        {
            get { return _maxHP; }
        }

        public int CurrentHP
        {
            get { return _currentHP; }
        }

        public int MaxEXP
        {
            get { return _maxEXP; }
        }

        public int CurrentEXP
        {
            get { return _currentEXP; }
        }


        public float CriticalPower
        {
            get { return _criticalPower; }
        }

        public float CriticalProb
        {
            get { return _criticalProb; }
        }
        public List<Item> Inventory
        {
            get { return _inventory; }
        }

        private Weapon _equipedWeapon;

        private int _attPower;
        private int _defPower;

        private float _criticalPower;
        private float _criticalProb;

        private int _level;
        private int _statPoint;
        private int _weaponSkilled;

        
        private int _maxHP;
        private int _currentHP;

        private int _maxEXP;
        private int _currentEXP;

        List<Item> _inventory;
    }

}

코드를 보면 멤버변수에 인벤토리가 있는데, item 클래스를 만들어서 이를 상속받은 아이템들을 저장할 것이다.

Item클래스는 아래와 같다.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class Item
    {
        protected Item()
        {
        }

        public int AttPower
        {
            get { return _attPower; }
            protected set { _attPower = value; }
        }
        public int DefPower
        {
            get { return _defPower; }
            protected set { _defPower = value; }
        }

        public int WeaponSkilled
        {
            get { return _weaponSkilled; }
            protected set { _weaponSkilled = value; }
        }

        public int HealHP
        {
            get { return _healHP; }
            protected set { _healHP = value; }
        }

        public float CriticalPower
        {
            get { return _criticalPower; }
            protected set { _criticalPower = value; }
        }

        public float CriticalProb
        {
            get { return _criticalProb; }
            protected set { _criticalProb = value; }
        }

        public int RemainTurn
        {
            get { return _remainTurn; }
            protected set { _remainTurn = value; }
        }


        private int _attPower;
        private int _defPower;

        private float _criticalPower;
        private float _criticalProb;

        private int _weaponSkilled;

        private int _healHP;
        private int _remainTurn;
    }
    
    public class Apple : Item
    {
        public Apple()
        {
            HealHP = 50;
        }
    }
}

 

 소비 아이템만 만들 것이기 때문에, 이 아이템으로 인해 상승될 수 있는 옵션들을 저장하였다.

 

무기 클래스도 조금 개선하였다.

검 종류에는 출혈 확률을 추가하였고, 창 종류에는 회피 확률, 망치 종류에는 기절 확률을 추가하였다.

더보기
더보기
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;



namespace C_Study
{
    public enum WeaponType
    {
        None,
        Sword,
        Spear,
        Hammer,
    }

    public class Weapon
    {
        protected Weapon()
        {

        }
        public WeaponType WPType
        {
            get { return _wpType;  }
            protected set { _wpType = value; }
        }

        public string WPName
        {
            get { return _wpName; }
            protected set { _wpName = value; }
        }
        public int AttPower
        {
            get { return _attPower; }
            protected set { _attPower = value; }
        }
        public float CriticalProb
        {
            get { return _criticalProb; }
            protected set { _criticalProb = value; }
        }

        private int _attPower;
        private float _criticalProb;

        private string _wpName;
        private WeaponType _wpType;
    }

    public class Sword : Weapon
    {
        protected Sword()
        {
            WPType = WeaponType.Sword;
            CriticalProb = 25.0f;
        }

        protected float BleedingProb
        { 
            get { return _bleedingProb; }
            set { _bleedingProb = value; }  
        }

        private float _bleedingProb;
    }

    public class Spear : Weapon
    {
        protected Spear()
        {
            WPType = WeaponType.Spear;
            CriticalProb = 15.0f;
        }

        protected float EvadingProb
        {
            get { return _evadingProb; }
            set { _evadingProb = value; }
        }

        private float _evadingProb;
    }

    public class Hammer : Weapon
    {
        protected Hammer()
        {
            WPType = WeaponType.Hammer;
            CriticalProb = 5.0f;
        }

        protected float StunProb
        {
            get { return _stunProb; }
            set { _stunProb = value; }
        }

        private float _stunProb;
    }

    public class OldSword : Sword
    {
        public OldSword()
        {
            WPName = "오래된 검";
            AttPower = 10;
            BleedingProb = 5.0f;
        }
    }

    public class OldSpear : Spear
    {
        public OldSpear()
        {
            WPName = "오래된 창";
            AttPower = 15;
            EvadingProb = 10.0f;
        }
    }

    public class OldHammer : Hammer
    {
        public OldHammer()
        {
            WPName = "오래된 망치";
            AttPower = 30;
            StunProb = 5.0f;
        }
    }
}

아래는 지금까지 구현된 것을 영상으로 찍은 것이다.

 

C++의 문법에 점점 익숙해지는중..

'프로젝트 > C# 머드게임' 카테고리의 다른 글

C# 머드 게임 프로젝트 (4)  (0) 2024.08.01
C# 머드 게임 프로젝트 (3)  (0) 2024.07.30
C# 머드 게임 프로젝트 (1)  (0) 2024.07.28

C# 공부를 좀 재밌게 할 수 있는 방법 없나 찾아보다가 문득 머드게임이 떠올랐다.

머드게임을 즐겨 하던 세대는 아니지만, 머드게임을 구현하면서 C#을 공부하면 참 재밌을 것 같았다.

 

이름은 전사의 모험 RPG로 간단하게 정해봤다.

그냥 무난하게 턴제 형식의 전투를 만들 생각이다.

 

using C_Study;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

class MainClass
{
    public static void Main()
    {
        GameCore gameCore = new GameCore();
        
        gameCore.Start();
        gameCore.Update();
        gameCore.End();
    }
}

GameCore라는 클래스 내부에서 게임을 전체적으로 관리할 것이다.

 

Main함수에선 인스턴스의 Start, Update, End를 호출하도록 하였다.

Start에선 게임이 시작되기 전에 설정해야 할 것들을 세팅하는 함수이고 Update는 게임이 진행되는 함수이다.

End는 게임이 종료될 때 해야 할 일을 하는 함수이다.

 

아래는 GameCore 클래스 내부 코드이다.

using C_Study;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace C_Study
{
    public enum LevelType
    {
        Start,
        Play,
    }

    public class GameCore
    {
        public void Start()
        {
            _player = new Player();
            _levelType = LevelType.Start;
        }

        public void Update()
        {
            while(_player != null)
            {
                switch (_levelType)
                {
                    case LevelType.Start:
                        StartLevelUpdate();
                        break;
                    case LevelType.Play:
                        PlayLevelUpdate();
                        break;
                }
            }
        }

        public void End()
        {

        }

        private void StartLevelUpdate()
        {
            while (_levelType == LevelType.Start)
            {
                Console.Clear();

                Console.WriteLine("*************************************************");
                Console.WriteLine("*************************************************");
               
                Console.Write("*****************");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("전사의 모험 RPG");
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("*****************");

                Console.Write("******************");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("제작자 오의현");
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("******************");

                Console.WriteLine("*************************************************");
                Console.WriteLine("*************************************************");
                Console.WriteLine();

                Console.WriteLine("모험을 시작하시겠습니까?");

                Console.ForegroundColor = ConsoleColor.Blue;
                Console.WriteLine("1 : YES");
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("2 : NO");
                Console.ForegroundColor = ConsoleColor.White;

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                if(input.Length > 1)
                {
                    continue;
                }
                
                int toInt = int.Parse(input);

                switch (toInt)
                {
                    case 1:
                        _levelType = LevelType.Play;
                        break;
                    case 2:
                        Environment.Exit(0);
                        break;
                }
            }
        }
        private void PlayLevelUpdate()
        {
            while (_levelType == LevelType.Play)
            {
                Console.Clear();

                Console.WriteLine("**************************************************");
                Console.WriteLine("*****시작*****");
                Console.WriteLine("**************************************************");
                Console.WriteLine();

                string input = Console.ReadLine();

                if (DevFunctions.IsNumeric(input) == false)
                {
                    continue;
                }

                int toInt = int.Parse(input);
            }
        }

        private Player _player;
        private LevelType _levelType;
    }
}

 

Start에선 일단 플레이어 생성과 최초 레벨 설정만 해주었다.

Update에선 Level에 따라 다른 함수가 실행되도록 하였다.

 

중간에 DevFunctions.IsNumeric()이라는 함수가 보이는데 이는 입력받은 문자열이 정수로만 이루어져있는지 판단하는 함수이다. 

 

내부 코드는 아래와 같다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public class DevFunctions
    {
        public static bool IsNumeric(string str)
        {
            if (string.IsNullOrEmpty(str) == true)
            {
                return false;
            }

            foreach (char curChar in str)
            {
                if (char.IsDigit(curChar) == false)
                {
                    return false;
                }
            }

            return true;
        }
    }
}

 

StartLevel은 게임시작 화면이다.

이런 식으로 화면에 출력된다. 시작 창을 화려하게 꾸며보고 싶었는데, 그런 쪽에는 재능이 없는지라 심플하게 갔다.

1번을 누르면 게임이 시작되고 2번을 누르면 콘솔이 종료된다.

 

PlayLevel을 구현하기 전에 무기 클래스를 먼저 구현해놓았다. 무기 클래스 코드는 아래와 같다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;

namespace C_Study
{
    public enum WeaponType
    {
        None,
        Sword,
        Spear,
        Hammer,
    }

    public class Weapon
    {
        protected Weapon()
        {

        }

        protected WeaponType WPType
        {
            get { return _wpType;  }
            set { _wpType = value; }
        }

        protected string WPName
        {
            get { return _wpName; }
            set { _wpName = value; }
        }
        protected int AttPower
        {
            get { return _attPower; }
            set { _attPower = value; }
        }
        protected float BonusAttackProb
        {
            get { return _bonusAttackprob; }
            set { _bonusAttackprob = value; }
        }

        private int _attPower;
        private float _bonusAttackprob;

        private string _wpName;
        private WeaponType _wpType;
    }

    public class Sword : Weapon
    {
        protected Sword()
        {
            WPType = WeaponType.Sword;
            BonusAttackProb = 25.0f;
        }
    }

    public class Spear : Weapon
    {
        protected Spear()
        {
            WPType = WeaponType.Spear;
            BonusAttackProb = 15.0f;
        }
    }

    public class Hammer : Weapon
    {
        protected Hammer()
        {
            WPType = WeaponType.Hammer;
            BonusAttackProb = 5.0f;
        }
    }

    public class OldSword : Sword
    {
        public OldSword()
        {
            WPName = "오래된 검";
            AttPower = 10;
        }
    }

    public class OldSpear : Spear
    {
        public OldSpear()
        {
            WPType = WeaponType.Spear;
            WPName = "오래된 창";
            AttPower = 15;
        }
    }

    public class OldHammer : Hammer
    {
        public OldHammer()
        {
            WPType = WeaponType.Hammer;
            WPName = "오래된 망치";
            AttPower = 30;
        }
    }
}

 

Weapon 클래스를 Spear, Sword, Hammer 클래스가 상속받도록 하였고 실제로 플레이어가 장착할 무기들은 종류에 맞는 클래스를 상속받도록 하였다.

 

코드의 아래쪽에 보면 OldSword, OldSpear, OldHammer가 있는데 이게 플레이어가 실제 장착할 아이템이다.

처음에 장착할 기본템이며 일단은 이 3개만 만들어두었다.

 

무기는 2개의 스탯을 가지고 있다. 공격력과 추가공격확률이다.

 

공격력은 말 그대로 공격력이고, 추가공격확률은 공격시에 추가적으로 공격이 발생할 확률이다.

검은 공격력이 낮은 대신 추가공격확률이 높고, 망치는 공격력이 높은 대신 추가공격확률이 낮다.

창은 그 중간쯤이다.

 

일단은 이정도만 구현해보았다. 

다음엔 게임 플레이 내용을 본격적으로 구현할 것이다.

STL의 vector 템플릿은 bool 자료형에 대해 특수화되어있다.

bool타입은 기본 1바이트를 사용하지만, 비트연산을 활용하면 1바이트에 8개의 boolean값을 저장할 수 있기 때문이다.

이로 인한 여러 문제들도 있다지만, STL을 모방하는 프로젝트인만큼 bool 타입에 특수화를 진행하였다. 

 

일단은 생성자 부분만 정의하였고, 정의된 내용은 아래 코드와 같다.

다음엔 [], * 등 연산자를 오버로딩할 것이며 이터레이터를 추가하여 begin, end 등의 함수를 정의할 것이다.

template <>
class Vector<bool>
{
public:
    //Default
    Vector() : MySize(0), MyCapacity(32)
    {
        if (MyElements == nullptr)
        {
            MyElements = new unsigned int[MyCapacity]();
            BeginPtr = MyElements;
        }
    }

    //Only Size
    Vector(const size_t _Size)
    {
        if (BeginPtr == nullptr)
        {
            MySize = _Size;
            MyCapacity = _Size + (32 - (_Size % 32));

            MyElements = new unsigned int[MyCapacity](0);
            BeginPtr = MyElements;
        }
    }
    
    //Size, Data
    Vector(const size_t _Size, const bool _Data)
    {
        if (BeginPtr == nullptr)
        {
            MySize = _Size;
            MyCapacity = _Size + (32 - (_Size % 32));

            MyElements = new unsigned int[MyCapacity](0);
            BeginPtr = MyElements;
            
            if (_Data == true)
            {
                RangedBitOn(0, 0, _Size / 32, _Size % 32);
            }
            else
            {
                RangedBitOff(0, 0, _Size / 32, _Size % 32);
            }
        }
    }

    ~Vector()
    {
        if (MyElements != nullptr)
        {
            delete[] MyElements;

            BeginPtr = nullptr;
            MyElements = nullptr;
        }
    }

private:
    void BitOff(size_t _Index, size_t _Bit)
    {
        MyElements[_Index] &= ~(1 << _Bit);
    }

    void BitOn(size_t _Index, size_t _Bit)
    {
        MyElements[_Index] |= (1 << _Bit);
    }

    void RangedBitOn(size_t _StartIndex, size_t _StartBit, size_t _EndIndex, size_t _EndBit)
    {
        size_t Start = _StartIndex * 32 + _StartBit;
        size_t End = _EndIndex * 32 + _EndBit;

        for (size_t i = Start; i < End; i++)
        {
            size_t Index = i / 32;
            size_t Bit = i % 32;

            MyElements[Index] |= (1 << Bit);
        }
    }

    void RangedBitOff(size_t _StartIndex, size_t _StartBit, size_t _EndIndex, size_t _EndBit)
    {
        size_t Start = _StartIndex * 32 + _StartBit;
        size_t End = _EndIndex * 32 + _EndBit;

        for (size_t i = Start; i < End; i++)
        {
            size_t Index = i / 32;
            size_t Bit = i % 32;

            MyElements[Index] &= ~(1 << Bit);
        }
    }

private:
    unsigned int* BeginPtr = nullptr;
    unsigned int* MyElements = nullptr;

    size_t MySize = 0;
    size_t MyCapacity = 0;
};

생성자, 소멸자를 정의하였고 reserve, resize, push_back, emplace_back 를 정의하였다.

기존에 알고 있던 STL의 작동 원리를 바탕으로 만들어보았다.

 

아래는 현재까지 작성된 코드 전문이다.

#pragma once
#include <memory>

template<typename DataType>
class Vector 
{
public:
    //Default
    Vector() : MySize(0), MyCapacity(4)
    {
        if (MyElements == nullptr)
        {
            MyElements = new DataType[MyCapacity]();
            
            BeginPtr = MyElements;
            EndPtr = MyElements;
        }
    }

    //Only Size
    Vector(const size_t _Size) : MySize(_Size), MyCapacity(_Size)
    {
        if (BeginPtr == nullptr)
        {
            MyElements = new DataType[MyCapacity]();

            BeginPtr = MyElements;
            EndPtr = BeginPtr + _Size;
        }
    }

    //Size, Data (L-Value)
    Vector(const size_t _Size, const DataType& _Data) : MySize(_Size), MyCapacity(_Size)
    {
        if (BeginPtr == nullptr)
        {
            MyElements = new DataType[MyCapacity];

            BeginPtr = MyElements;
            EndPtr = BeginPtr + _Size;

            for (size_t i = 0; i < _Size; i++)
            {
          	    MyElements[i] = _Data;
            }
        }
    }

    //Size, Data (R-Value)
    Vector(const size_t _Size, DataType&& _Data) : MySize(_Size), MyCapacity(_Size)
    {
        if (BeginPtr == nullptr)
        {
            MyElements = new DataType[MyCapacity];

            BeginPtr = MyElements;
            EndPtr = BeginPtr + _Size;

            for (size_t i = 0; i < _Size; i++)
            {
                MyElements[i] = std::forward<DataType>(_Data);
            }
        }
    }

    ~Vector()
    {
        if (MyElements != nullptr)
        {
            delete[] MyElements;

            BeginPtr = nullptr;
            EndPtr = nullptr;
            MyElements = nullptr;
        }
    }
    
public:
    void Reserve(const size_t _Capacity)
    {
        ReAllocate(_Capacity);
    }

    void Resize(const size_t _Size)
    {
        ReAllocate(_Size);
        MySize = _Size;
    }

    void Resize(const size_t _Size, const DataType& _Data)
    {
        ReAllocate(_Size);

        for (int i = MySize; i < _Size; i++)
        {
            MyElements[i] = _Data;
        }

        MySize = _Size;
    }

    void Resize(const size_t _Size, DataType&& _Data)
    {
        ReAllocate(_Size);

        for (size_t i = MySize; i < _Size; i++)
        {
            MyElements[i] = std::forward<DataType>(_Data);
        }

        MySize = _Size;
    }

public:
    template<typename... Valty>
    void Emplace_Back(Valty&&... _Value)
    {
        if (MySize == MyCapacity)
        {
            ReAllocate(MyCapacity * 2);
        }

        new (&MyElements[MySize]) DataType(std::forward<Valty>(_Value)...);

        ++MySize;
        ++EndPtr;
    }

    void Push_Back(const DataType& _Data)
    {
        if (MySize == MyCapacity)
        {
            ReAllocate(MyCapacity * 2);
        }
    
        MyElements[MySize] = _Data;
        
        ++MySize;
        ++EndPtr;
    }
    
    void Push_Back(DataType&& _Data)
    {
        if (MySize == MyCapacity)
        {
            ReAllocate(MyCapacity * 2);
        }
    
        MyElements[MySize] = std::forward<DataType>(_Data);
        
        ++MySize;
        ++EndPtr;
    }

private:
    void ReAllocate(const size_t _Capacity)
    {
        if (_Capacity <= MyCapacity)
        {
            return;
        }


        DataType* NewPtr = new DataType[_Capacity]();

        for (size_t i = 0; i < MySize; i++)
        {
            NewPtr[i] = std::move(MyElements[i]);
        }

        BeginPtr = NewPtr;
        EndPtr = BeginPtr + MySize;

        delete[] MyElements;

        MyElements = NewPtr;

        MyCapacity = _Capacity;
    }

private:
    DataType* BeginPtr = nullptr;
    DataType* EndPtr = nullptr;

    DataType* MyElements = nullptr;
    
    size_t MySize = 0;
    size_t MyCapacity = 0;
};

기존에는 각 렌더러의 Update에서 트랜스폼을 업데이트 해주고 있었는데, 이젠 세팅만 해주고 엔진에서 트랜스폼 업데이트 함수를 호출하도록 수정하였다.

 

 

Renderer의 멤버변수에 크기, 위치, 회전값을 만들어주었다.

로컬트랜스폼과 월드트랜스폼도 구분해야 하는데, 나중에 추가할 것이고 일단은 하나만 두었다.

 

멤버함수를 호출하여 값을 설정할 수 있다.

 

Renderer의 멤버함수로 트랜스폼 업데이트 위치를 옮겨주었다.

엔진에서 렌더러의 업데이트를 호출한 뒤에, 렌더러의 트랜스폼 업데이트를 호출해주도록 변경하였다.

 

각 렌더러의 Init함수에서 초기값을 세팅해주기만 하면 된다.

물론 업데이트에서 트랜스폼값을 변경해도 된다.

 

기존과 동일하게 실행이 잘 된다!

지난 번에 일단 블러까지는 적용이 잘 되는 것을 확인하였다.

이제, 본격적으로 블룸을 적용해보자.

 

먼저, 여러개의 렌더타겟을 사용해야하기 때문에 외부에서도 렌더타겟을 만들어서 사용할 수 있도록 함수를 추가해주었다.

RenderTarget EngineBase::CreateRenderTarget(UINT _Width, UINT _Height)
{
    RenderTarget NewRenderTarget;

    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;

    D3D11_TEXTURE2D_DESC txtDesc;
    ZeroMemory(&txtDesc, sizeof(txtDesc));
    txtDesc.Width = _Width;
    txtDesc.Height = _Height;
    txtDesc.MipLevels = txtDesc.ArraySize = 1;
    txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    txtDesc.SampleDesc.Count = 1;
    txtDesc.Usage = D3D11_USAGE_DEFAULT;
    txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
    txtDesc.MiscFlags = 0;
    txtDesc.CPUAccessFlags = 0;

    D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
    viewDesc.Format = txtDesc.Format;
    viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    viewDesc.Texture2D.MipSlice = 0;

    HRESULT Result;
    Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D() failed" << std::endl;
    }

    Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, NewRenderTarget.RTV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateRenderTargetView() failed" << std::endl;
    }

    Result = Device->CreateShaderResourceView(Texture.Get(), nullptr, NewRenderTarget.SRV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateShaderResourceView() failed" << std::endl;
    }

    return NewRenderTarget;
}

해당 함수를 사용하면, 렌더타겟의 SRV와 RTV를 반환해주기 때문에 이를 활용해서 렌더링할 수 있게 된다.

 

DetectTarget = EngineBase::GetInstance().CreateRenderTarget(WindowSize.first, WindowSize.second);
BlurTarget = EngineBase::GetInstance().CreateRenderTarget(BlurData.Width, BlurData.Height);

먼저 렌더타겟을 생성해주었다.

float ClearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

//추출
SetTexture(DoubleBufferSRV);

EngineBase::GetInstance().GetContext()->ClearRenderTargetView(DetectTarget.RTV.Get(), ClearColor);
EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, DetectTarget.RTV.GetAddressOf(), DepthStencilView.Get());

PostProcessRenderer->SetPSShader(L"BrightDetectPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

그리고 PostProcess클래스의 Render 부분에서 이렇게 DetectTarget 렌더타겟에 밝은 부분을 추출하도록 하였다.

#include "LightHeader.hlsli"

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
    
    float Luminance = 0.2126f * Color.r + 0.7152f * Color.g + 0.0722f * Color.b;
    
    if (Luminance > 0.8f)
    {
        return Color;
    }
    else
    {
        return (float4) 0.0f;
    }
}

 

추출하는 쉐이더 코드는 위와 같다. 픽셀의 휘도를 계산하여 일정 수치 이상이라면 해당 색상을 렌더타겟에 렌더링하도록 하였다.

 

//블러
SetTexture(DetectTarget.SRV);
EngineBase::GetInstance().GetContext()->RSSetViewports(1, &DownViewPort);

EngineBase::GetInstance().GetContext()->ClearRenderTargetView(BlurTarget.RTV.Get(), ClearColor);
EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, BlurTarget.RTV.GetAddressOf(), DepthStencilView.Get());

PostProcessRenderer->SetPSShader(L"BlurPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

EngineBase::GetInstance().GetContext()->RSSetViewports(1, &UpViewPort);

다음은 블러이다.

추출된 렌더타겟을 BlurPixelShader에서 사용할 텍스쳐로 연결해주었다.

그리고, 해당 텍스쳐에 블러처리를 해주었다.

#include "LightHeader.hlsli"

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
};

cbuffer EBloomData : register(b0)
{
    int Width;
    int Height;
    int Padding1;
    int Padding2;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

static float Gau[5][5] =
{
    { 1, 4, 6, 4, 1 },
    { 4, 16, 24, 16, 4 },
    { 6, 24, 36, 24, 6 },
    { 4, 16, 24, 16, 4 },
    { 1, 4, 6, 4, 1 }
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = (float4)0.0f;
    
    float WidthRatio = 1.0f / (float) Width;
    float HeightRatio = 1.0f / (float) Height;
    
    float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
    
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
            StartTexCoord.y += HeightRatio;
        }
        
        StartTexCoord.x += WidthRatio;
        StartTexCoord.y = _Input.TexCoord.y - HeightRatio * 2;
    }

    Color /= 256.0f;
    
    return Color;
}

블러는 앞의 게시물에서 본 것과 동일하게 가우시안 필터를 사용하여 구현하였다.

 

이 때, 포스트프로세스 클래스의 코드를 보면 뷰포트를 세팅해주고 있는데 이유는 블러를 할 때 다운샘플링을 했기 때문이다. 블러의 범위를 넓게 적용하기 위해 해상도를 낮춘채로 블러를 적용하였고, 화면에 제대로 렌더링되도록 하기 위해 뷰포트와 크기를 맞춰준 것이다.

////병합
SetTexture(BlurTarget.SRV, 0);

EngineBase::GetInstance().GetContext()->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
EngineBase::GetInstance().GetContext()->OMSetBlendState(BlendState.Get(), NULL, 0xFFFFFFFF);

PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");
PostProcessRenderer->Render(_DeltaTime);

블러까지 적용한 뒤, 기존 이미지에 합성해주었다.

블러된 이미지가 더블버퍼의 원본 이미지와 제대로 합성될 수 있도록 BlendState도 만들어주었다.

void BloomPostProcess::CreateBlendState()
{
	D3D11_BLEND_DESC Desc = { 0, };

	Desc.AlphaToCoverageEnable = false;
	Desc.IndependentBlendEnable = false;
	Desc.RenderTarget[0].BlendEnable = true;
	Desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
	Desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
	Desc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE;
	Desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;

	Desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_MAX;
	Desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
	Desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;

	EngineBase::GetInstance().GetDevice()->CreateBlendState(&Desc, BlendState.GetAddressOf());
}

 

결과는 위와 같다.

디테일은 많이 떨어지지만, 일단 블룸 자체는 잘 적용되는 듯 하다.

제대로 하려면, 다운샘플링하는 과정과 업샘플링하는 과정을 여러번 반복해야하지만, 아직은 이정도로 만족하자.

 

이제, 어느정도 쉐이더를 적용해보았으니 이제는 엔진 자체를 개선할 때가 된 것 같다.

엔진 내에 하드코딩된 부분도 너무 많고, 너무 안좋게 설계된 부분이 많다고 생각되기 때문에 천천히 엔진을 고쳐나가보자!

오브젝트에 대한 렌더링을 모두 마친 이후에, 전체 화면을 대상으로 추가적인 후처리를 적용하는 것을 포스트 프로세스라고 한다. 포스트 프로세스를 프로젝트에 구현해볼 생각이다.

 

먼저, 렌더링 구조를 살짝 바꿔주었다.

기존에는 오브젝트를 백버퍼에 바로 그리는 형식이었지만, 현재는 렌더타겟을 추가로 만들고 해당 렌더타겟에 오브젝트를 모두 그린 뒤에, 후처리를 적용하여 백버퍼에 복사하여 그리는 구조로 바꿔주었다.

BOOL EngineBase::CreateDoubleBuffer()
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;

    D3D11_TEXTURE2D_DESC txtDesc;
    ZeroMemory(&txtDesc, sizeof(txtDesc));
    txtDesc.Width = WindowWidth;
    txtDesc.Height = WindowHeight;
    txtDesc.MipLevels = txtDesc.ArraySize = 1;
    txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 이미지 처리용도
    txtDesc.SampleDesc.Count = 1;
    txtDesc.Usage = D3D11_USAGE_DEFAULT;
    txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
    txtDesc.MiscFlags = 0;
    txtDesc.CPUAccessFlags = 0;
    
    D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
    viewDesc.Format = txtDesc.Format;
    viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    viewDesc.Texture2D.MipSlice = 0;

    HRESULT Result;
    Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D() failed" << std::endl;
        return FALSE;
    }

    Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, DoubleBufferRTV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateRenderTargetView() failed" << std::endl;
        return FALSE;
    }

    Result = Device->CreateShaderResourceView(Texture.Get(), nullptr, DoubleBufferSRV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateShaderResourceView() failed" << std::endl;
        return FALSE;
    }

    return TRUE;
}

 

이렇게, SRV와 RTV를 모두 생성해주었고, 이 doublebuffer에 1차적인 렌더링이 모두 진행될 것이다.

다음은 그렇게 그려진 렌더링 장면에 포스트 프로세스를 적용하기 위해, 화면 크기만한 사각형 메쉬를 만들어주었다.

 

#include "ScreenRenderer.h"

ScreenRenderer::ScreenRenderer()
{
}

ScreenRenderer::~ScreenRenderer()
{
}

void ScreenRenderer::Init()
{
    Renderer::Init();

    SetModelToSquare(1.0f);
    SetTransform();
}

void ScreenRenderer::Update(float _DeltaTime)
{

}

void ScreenRenderer::SetTransform()
{
    TransFormData.WorldMatrix = DirectX::SimpleMath::Matrix::CreateScale(1.0f) * DirectX::SimpleMath::Matrix::CreateRotationY(0.0f) *
        DirectX::SimpleMath::Matrix::CreateTranslation(DirectX::SimpleMath::Vector3(0.0f, 0.0f, 0.0f));

    TransFormData.WorldMatrix = TransFormData.WorldMatrix.Transpose();

    TransFormData.ViewMAtrix = EngineBase::GetInstance().ViewMat;
    TransFormData.ViewMAtrix = TransFormData.ViewMAtrix.Transpose();

    TransFormData.ProjMatrix =
        DirectX::XMMatrixOrthographicOffCenterLH(-1.0f, 1.0f, -1.0f, 1.0f,
            0.01f, 100.0f);

    TransFormData.ProjMatrix = TransFormData.ProjMatrix.Transpose();

    TransFormData.InvTranspose = TransFormData.WorldMatrix;
    TransFormData.InvTranspose.Translation({ 0.0f, 0.0f, 0.0f });
    TransFormData.InvTranspose = TransFormData.InvTranspose.Transpose().Invert();
}

 

다른 렌더러와 다를거 없어보이지만, 위의 렌더러는 직교투영을 적용하였다. 화면의 범위를 매쉬 크기에 맞춰주었기 떄문에 메쉬의 표면에 색을 입히면 화면 전체에 입혀지게 된다.

 

이 매쉬의 텍스쳐로 doublebuffer의 SRV를 사용할 것이다.

 

이 렌더러를 이용하여 포스트 프로세스를 적용할 것이기 때문에 포스트 프로세스 클래스도 만들어주었다.

 

#pragma once
#include "BaseHeader.h"

class PostProcess
{

public:

    PostProcess();
    ~PostProcess();

    PostProcess(const PostProcess& _Other) = delete;
    PostProcess(PostProcess&& _Other) noexcept = delete;
    PostProcess& operator=(const PostProcess& _Other) = delete;
    PostProcess& operator=(PostProcess&& _Other) noexcept = delete;

    void SetTexture(Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> _SRV);

    virtual void Init();
    virtual void Render(float _DeltaTime);

protected:
    std::shared_ptr<class ScreenRenderer> PostProcessRenderer;

private:
};

 

각 포스트 프로세스는 위에서 만든 ScreenRenderer를 반드시 보유하도록 하였다.

이를 상속받아 여러 종류의 포스트 프로세스를 생성할 것이다.

 

void EngineBase::Render(float _DeltaTime)
{
    float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
    Context->ClearRenderTargetView(DoubleBufferRTV.Get(), clearColor);
    Context->ClearDepthStencilView(DepthStencilView.Get(),
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    Context->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
    Context->OMSetDepthStencilState(DepthStencilState.Get(), 0);
    
    if (isWireFrame == false)
    {
        Context->RSSetState(SolidRasterizerState.Get());
    }
    else
    {
        Context->RSSetState(WireRasterizerState.Get());
    }

    for (std::shared_ptr<Renderer> Renderer : Renderers)
    {
        Renderer->Render(_DeltaTime);
    }

    Context->ClearRenderTargetView(BackBufferRTV.Get(), clearColor);
    Context->OMSetRenderTargets(1, BackBufferRTV.GetAddressOf(), DepthStencilView.Get());

    for (std::shared_ptr<PostProcess> PostProcess : PostProcesses)
    {
        PostProcess->SetTexture(DoubleBufferSRV);
        PostProcess->Render(_DeltaTime);
    }
}

 

엔진의 렌더링 마지막 부분에 PostProcess의 Render를 차례대로 호출해주는 것을 몰 수 있다.

포스트 프로세스 클래스는 생성할 때마다 Engine의 자료구조에 삽입되도록 하였다.

 

(현재는 포스트 프로세스를 적용하지 않으면 모니터에 렌더링이 안되는 괴상한 구조이다. 일단 기능을 구현하느라 이런 구조가 되었는데, 블룸을 적용한 뒤에 엔진 구조를 수정할 예정이라 일단은 그대로 두었다.)

 

BloomShader를 한 번 적용해볼것이다.

먼저, BloomShader는 세가지 단계로 이루어진다.

 

1. 밝은 부분을 추출한다.

2. 화면에 블러처리를 한다.

3. 기존의 화면에 추출된 밝은 색상을 더해준다.

 

그러므로 먼저 블러처리에 대한 포스트 프로세스가 잘 적용되는지부터 테스트해볼것이다.

#pragma once
#include "PostProcess.h"

struct EBloomData
{
	int Width = 1600;
	int Height = 900;
	int Padding1 = 0;
	int Padding2 = 0;
};

class BloomPostProcess : public PostProcess
{

public:

	BloomPostProcess();
	~BloomPostProcess();

	BloomPostProcess(const BloomPostProcess& _Other) = delete;
	BloomPostProcess(BloomPostProcess&& _Other) noexcept = delete;
	BloomPostProcess& operator=(const BloomPostProcess& _Other) = delete;
	BloomPostProcess& operator=(BloomPostProcess&& _Other) noexcept = delete;

	virtual void Init() override;
	virtual void Render(float _Deltatime) override;

protected:
	
private:
	EBloomData BloomData;
};

 

포스트프로세스 클래스를 상속받은 BloomPostProcess 클래스를 위와 같이 만들어주었따.

#include "BloomPostProcess.h"
#include "ScreenRenderer.h"

BloomPostProcess::BloomPostProcess()
{
}

BloomPostProcess::~BloomPostProcess()
{
}

void BloomPostProcess::Init()
{
	PostProcessRenderer = std::make_shared<ScreenRenderer>();
	PostProcessRenderer->Init();

	PostProcessRenderer->CreateConstantBuffer(EShaderType::PSShader, L"BloomPixelShader.hlsl", BloomData);

	PostProcessRenderer->SetVSShader(L"BloomVertexShader.hlsl");
	PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");

	PostProcessRenderer->SetSampler("LINEARWRAP");
}

void BloomPostProcess::Render(float _DeltaTime)
{
	PostProcessRenderer->Render(_DeltaTime);

	ID3D11ShaderResourceView* SRV = NULL;
	EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, &SRV);
}

 

초기화는 위와 같다. 렌더러를 먼저 만들어준뒤, 상수버퍼를 연결해주고 쉐이더 세팅을 해주었다.

그 이후, Render함수에선 렌더링을 진행한 뒤, SRV를 비워주도록 하였다.

 

#include "LightHeader.hlsli"

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
};

cbuffer EBloomData : register(b0)
{
    int Width;
    int Height;
    int Padding1;
    int Padding2;
};

Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);

static float Gau[5][5] =
{
    { 1, 4, 6, 4, 1 },
    { 4, 16, 24, 16, 4 },
    { 6, 24, 36, 24, 6 },
    { 4, 16, 24, 16, 4 },
    { 1, 4, 6, 4, 1 }
};

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = (float4)0.0f;
    
    float WidthRatio = 1.0f / (float) Width;
    float HeightRatio = 1.0f / (float) Height;
    
    float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
    
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            StartTexCoord.y += HeightRatio;
            Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
        }
        
        StartTexCoord.x += WidthRatio;
        StartTexCoord.y = _Input.TexCoord.y;
    }

    Color /= 256.0f;
    
    return Color;
}

 

위는 블러를 적용하는 픽셀쉐이더 코드이다.

전형적인 가우시안 블러 코드를 그대로 구현하였다.

 

좌측이 기본이고 우측이 블러를 적용한 상태이다.

다행히 잘 작동되는 듯 하다.

다음엔 이걸 토대로 더 많은 렌더타겟을 사용하여 블룸효과를 완전히 구현할 것이다.

그 다음엔 엔진 구조를 다소 개편해보고자 한다.

 

기존에는 CubeMap은 무조건 dds파일로, 일반 텍스쳐는 다른 확장자로 로드하는 이상한 구조였다.

이번엔, 확장자 별로 로드방식을 다르게 하되, 큐브맵 텍스쳐와 일반적인 디퓨즈 텍스쳐를 구분하여 로드하도록 수정할 것이다.

void ResourceManager::LoadTexture(const std::string& _TextureName, ETextureType _Type)
{
	std::string Format = GetFormat(_TextureName);

	if (Format == "dds")
	{
		LoadDDSTexture(_TextureName, _Type);
	}
	else
	{
		LoadGeneralTexture(_TextureName, _Type);
	}
}

 

먼저 LoadTexture 함수를 위와 같이 수정하였다.

파일 이름에서 확장자를 탐색한 뒤, 확장자를 기준으로 dds라면 LoadDDSTexture함수를 호출하였고 아니라면 LoadGeneralTexture함수를 호출하였다.

 

이렇게 확장자를 구분한 이유는 stb 라이브러리는 dds파일을 지원하지 않는 것도 있고, dds파일의 경우 마이크로 소프트에서 다이렉트X에 맞게 만든 확장자이다 보니 DirectX 함수를 사용하는 것이 여러모로 좋을 것 같기 때문이기도 하다.

 

std::string ResourceManager::GetFormat(const std::string& _FileName)
{
    int Count = 0;

    for (int i = _FileName.size() - 1; i >= 0; i--)
    {
        if (_FileName[i] == '.')
        {
            break;
        }

        Count++;
    }

    std::string Format = _FileName.substr(_FileName.size() - Count, Count);

    return Format;
}

 

GetFormat함수 내부는 위와 같다.

void ResourceManager::LoadDDSTexture(const std::string& _TextureName, ETextureType _Type)
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    std::wstring Path = L"../Texture/";
    std::wstring TextureName;
    TextureName.assign(_TextureName.begin(), _TextureName.end());

    Path += TextureName;

    UINT Flag = 0;

    if (_Type == ETextureType::CubeMap)
    {
        Flag = D3D11_RESOURCE_MISC_TEXTURECUBE;
    }

    HRESULT Result = DirectX::CreateDDSTextureFromFileEx(
        EngineBase::GetInstance().GetDevice().Get(), Path.c_str(), 0, D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE, 0,
        Flag,
        DirectX::DDS_LOADER_FLAGS(false), (ID3D11Resource**)Texture.GetAddressOf(),
        SRV.GetAddressOf(), nullptr);

    if (Result != S_OK)
    {
        std::cout << "LoadCubeMapTexture failed " << std::endl;
        return;
    }

    TextureData NewTextureData;
    NewTextureData.Texture = Texture;
    NewTextureData.ShaderResourceView = SRV;

    LoadedTextures.insert({ _TextureName, NewTextureData });
}

 

위 코드는 DDS 파일 로드 함수이다.

Cube텍스쳐의 경우, Flag를 설정하여 SRV를 Cube텍스쳐로 생성하도록 하였다.

void ResourceManager::LoadGeneralTexture(const std::string& _TextureName, ETextureType _Type)
{
    std::string Path = "../Texture/";
    Path += _TextureName;

    int Width = 0;
    int Height = 0;
    int Channels = 0;

    unsigned char* LoadedImage = stbi_load(Path.c_str(), &Width, &Height, &Channels, 0);
    if (LoadedImage == nullptr)
    {
        std::cout << "Image Load Failed" << std::endl;
        return;
    }

    std::vector<uint8_t> Image;

    Image.resize(Width * Height * Channels);
    memcpy(Image.data(), LoadedImage, Image.size() * sizeof(uint8_t));

    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    D3D11_TEXTURE2D_DESC TexDesc = {};
    TexDesc.Width = Width;
    TexDesc.Height = Height;
    TexDesc.MipLevels = TexDesc.ArraySize = 1;
    TexDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    TexDesc.SampleDesc.Count = 1;
    TexDesc.Usage = D3D11_USAGE_IMMUTABLE;
    TexDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;

    if(_Type == ETextureType::CubeMap)
    {
        TexDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
    }

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = Image.data();
    InitData.SysMemPitch = TexDesc.Width * sizeof(uint8_t) * Channels;

    HRESULT Result = EngineBase::GetInstance().GetDevice()->CreateTexture2D(&TexDesc, &InitData, Texture.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D failed " << std::endl;
        return;
    }

    Result = EngineBase::GetInstance().GetDevice()->CreateShaderResourceView(Texture.Get(), nullptr, SRV.GetAddressOf());
    if (Result != S_OK)
    {
        std::cout << "CreateTexture2D failed " << std::endl;
        return;
    }

    TextureData NewTextureData;
    NewTextureData.Texture = Texture;
    NewTextureData.ShaderResourceView = SRV;

    stbi_image_free(LoadedImage);

    LoadedTextures.insert({ _TextureName, NewTextureData });

    return;
}

 

위 코드는 DDS를 제외한 나머지 텍스쳐를 로드하는 코드이다.

stb라이브러리를 사용해서 로드하고 있으며, DDS파일과 동일하게 ETextureType에 따라 Flag를 설정하여 Cube텍스쳐는 그에 맞게 로드하도록 하였다.

 

 

기존과 동일하게 잘 렌더링이 된다!

 

(나중에 알게된 사실이지만 큐브맵 텍스쳐는 DDS만 된다고 한다. png나 jpg등으로 사용하려면 직접 조립해야한다고...)

환경매핑은 큐브매핑을 활용해서 물체에 주변 사물이 반사되어 비치는 것을 구현하는 기술이다.

 

먼저, 반사를 테스트 하기 위한 구체 렌더러 하나를 추가해주었다.

 

우측에 검정색 동그라미가 하나 보인다. 아직 텍스쳐를 입히지 않아서 새까맣게 보인다.

일단 환경매핑 쉐이더를 작성해보자.

버텍스 쉐이더는 기존과 완전히 동일하다.

 

#include "LightHeader.hlsli"

cbuffer WorldLightBuffer : register(b0)
{
    float3 EyeWorld;
    float Padding;
    
    Light Lights[LIGHT_NUM];
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

TextureCube CubeMapTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float3 EyeDir = _Input.WorldPos - EyeWorld;
    EyeDir = normalize(EyeDir);
    
    _Input.WorldNormal = normalize(_Input.WorldNormal);
    
    float3 RefEyeDir = reflect(EyeDir, _Input.WorldNormal);
    
    float4 Color = CubeMapTexture.Sample(Sampler, RefEyeDir);
    return Color;
}

 

이렇게 픽셀쉐이더를 작성해주었다.

이존의 배경을 만들기 위한 큐브매핑과 다르게 Light 정보를 상수버퍼로 연결해주었다.

일단은 빛을 적용하지 않고, EyeWorld만 가져와서 사용할 것이다.

 

물체가 반사되어 눈으로 들어오는 과정을 역산하여, 눈으로부터 나가는 광선이 반사되었을 때 큐브맵의 어디에 충돌하는가를 계산하여 색을 정하는 것이다.

 

공식은 매우매우 간단하다. EyeDir(눈이 물체를 바라보는 방향 벡터)를 WorldNormal에 대해 반사시킨 벡터로 큐브맵을 샘플링하면 된다.

 

텍스쳐도 적용하고, 해당 쉐이더를 연결해주면 아래와 같은 결과가 나온다.

 

 

구체에 배경이 잘 반사되어 비치는 것을 확인할 수 있다.

이번엔, 커다란 박스를 하나 만들어서 그 안쪽에 큐브맵을 매핑할 것이다.

이를 이용하여, 배경을 만들어볼 생각이다.

 

void ResourceManager::LoadTexture(const std::string& _TextureName, ETextureType _Type)
{
    if (_Type == ETextureType::Diffuse)
    {
        LoadDiffuseTexture(_TextureName);
    }
    else if (_Type == ETextureType::CubeMap)
    {
        LoadCubeMapTexture(_TextureName);
    }
}

 

먼저 텍스쳐를 로드하는 곳에서 디퓨즈텍스쳐인지 큐브맵 텍스쳐인지 분류하여 로드하도록 하였다.

이유는 로드를 하면서 SRV까지 만들어주고 있는데, 큐브맵은 세팅을 조금 다르게 해야하기 때문이다.

void ResourceManager::LoadCubeMapTexture(const std::string& _TextureName)
{
    Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> SRV;

    std::wstring Path = L"../Texture/";
    std::wstring TextureName;
    TextureName.assign(_TextureName.begin(), _TextureName.end());

    Path += TextureName;

    HRESULT Result = DirectX::CreateDDSTextureFromFileEx(
        EngineBase::GetInstance().GetDevice().Get(), Path.c_str(), 0, D3D11_USAGE_DEFAULT,
        D3D11_BIND_SHADER_RESOURCE, 0,
        D3D11_RESOURCE_MISC_TEXTURECUBE,
        DirectX::DDS_LOADER_FLAGS(false), (ID3D11Resource**)Texture.GetAddressOf(),
        SRV.GetAddressOf(), nullptr);

    if (Result != S_OK)
    {
        std::cout << "LoadCubeMapTexture failed " << std::endl;
        return;
    }

    TextureData NewTextureData;
    NewTextureData.Texture = Texture;
    NewTextureData.ShaderResourceView = SRV;

    LoadedTextures.insert({ _TextureName, NewTextureData });
}

 

큐브맵 텍스쳐를 로드하는 과정은 이와 같다.

큐브맵 텍스쳐를 로드하면서 DDS파일로 로드하다보니 코드가 위와 같아졌다.

 

그런데 구조가 조금 이상한 것 같다. 큐브맵 텍스쳐를 png로 사용할 수도 있고, 디퓨즈 텍스쳐를 dds로 사용할 수도 있기 때문에 조만간 텍스쳐 로드 과정을 수정할 계획이다.

 

 그리고 프로젝트 초반에 만들어두었던 박스형태의 매쉬를 사용할 것이다.

하지만, 기존의 매쉬는 상자 바깥부분에 텍스쳐를 매핑하고 있었지만, 큐브매핑을 하면서 안쪽에 텍스쳐를 매핑할 것이기 때문에 인덱스 버퍼를 뒤집어서 세팅해주었다.

 

그리고 큐브맵 전용 쉐이더도 하나 만들어주었다.

먼저, 빛과 같은 기능들은 모두 제거하고 트랜스폼만 상수버퍼로 전달해주었다.

버텍스 쉐이더는 기존과 동일한 코드로 만들었다. 완전히 동일한 코드이기 때문에 기존의 버텍스 쉐이더를 재활용해도 되지만, 나중에 큐브맵에만 적용할 무언가를 작성할 수도 있기 때문에 일단 분리하여 생성하였다.

 

픽셀 쉐이더는 아래와 같다.

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float2 TexCoord : TEXCOORD;
    
    float3 WorldPos : POSITION1;
    float3 WorldNormal : NORMAL;
};

TextureCube CubeMapTexture : register(t0);
SamplerState Sampler : register(s0);

float4 main(PixelShaderInput _Input) : SV_TARGET
{
    float4 Color = CubeMapTexture.Sample(Sampler, _Input.WorldPos.xyz);
    return Color;
}

 

정말 간단하다. 텍스쳐를 세팅할 때 큐브맵 텍스쳐로 설정하였다면, 샘플링하면서 알아서 큐브맵 매핑을 해준다.

 

실행해서 확인을 해보자.

 

카메라가 박스 범위를 벗어나지만 않으면 잘 작동하는 듯 하다.

이렇게 큐브맵도 세팅을 해보았다!

다음엔 환경매핑을 해볼 예정이다.

+ Recent posts