아직 다듬어지지 않은 필기글이기 때문에 틀린 내용이나 아직 정리가 덜 된 부분이 (분명히) 있습니다...
디자인 패턴
디자인 패턴은 게임 설계할 때, 초반에 결정됨. 그래서 초반에 잘 결정해야 함. 디자인 패턴은 모든 게임에 범용적으로 설정되지 않음.
게임마다 다른 특성을 가지고 있기에 게임마다 다른 디자인 패턴을 사용할 수밖에 없음. 사용하는 플랫폼(PC, Web, Mobile), 언어에 따라 디자인 패턴은 달라짐.
각 디자인 패턴의 특성을 알고 내가 만들 게임에는 어떤 패턴을 적용해야겠다 구상, 이후 직접 제작하면서 체득을 해야 함. 원본 코드를 찾은 후 내 게임에 맞추어 적용, 응용하는 것이 필요.
자료실
[GitHub] Unity-Programming-Patterns
게임
생성
- Builder : decorator에 생성 추가된 버전
- Singleton
Object, component, factory, decorator 생성 관련 패턴
로직
- update method (업데이트 패턴)
- state
관리
- object pool
Bytecode 모딩 관련. 본인 코드가 아닌데 확장해야 하는 경우
Double buffer
- buffer : stack 구조로 입력, 출력 등 대상을 쌓아두는 것.
- 버퍼가 하나만 있을 때의 문제 : lag이 생길 수 있음.
- Double buffer : 버퍼를 2개 두고 번갈아 가면서 사용.
- 더블 버퍼는 화면 렌더링에 국한된 개념은 아님
- 화면 렌더링의 경우 더블 버퍼를 사용해 뒷 화면에 그리고 나서 때가 되면 뒷 화면 것을 보여주고 기존에 보여주던 화면은 이제 클리어함.
Factory pattern
받는 parameter에 따라 다른 하위 클래스를 생성.
예시 : 캐릭터 직업 선택
- Factory class에 직업 번호를 넣어주면 캐릭터를 해당 직업으로 생성함.
- parameter에 따라 서로 다른 클래스를 생성해야 하는 경우 Factory 사용할 수 있음.
Decorator pattern
- 이미 만들어진 것을 꾸미는 것 (만들기+꾸미기 → Builder로 볼 수 있음)
- ex. 어떤 아이템을 들고 있느냐에 따라 행동 달라지게 디자인
구조 예시
- 추상 팩토리 (생성 함수) 를 상속하는 Concrete Factory 1, 2
- 꾸미기는 각 파츠에 대해 가능. Weapon이 있고 Shield 존재. Weapon의 하위에는 Sword, Sphere가 있을 것. Shield 하위에는 큰 방패 작은 방패 … 각 무기와 방패 생산하는 클래스 Factory를 또 만들어 둠.
자동차 예시
- Basic Car가 존재하고
- Auto Cruise 기능, Navigation 등의 기능이 존재한다 해보자.
- Car Deco 클래스는 차의 기능을 정의할 것.
RPG 예시
- CProperty (Attack, Fly, Defend 기능을 가지고 있는 클래스)
- Study (기능을 학습하는 클래스)
- Character
- IChar (인터페이스) - Character - CharStudying - 기능1, 기능 2 존재한다고 할 때 기능1,2의 생성자는 CharStudying을 생성하는 것이 아니라 Character를 생성해야 함.
public interface Character{
void Study();
}
public class MyChar implements Character
{
@Override
public void Study(){
//
}
}
public class StudyingChar implements Character
{
private Character char;
public StudyingChar(Character char){
this.char = char;
}
@Override
public void Study(){
car.Study();
}
}
public class Attack extends StudyingChar
{
public Attack(Character char){
super(char);
}
@Override
public void assemble(){
super.assemble();
putTextLight();
}
}
예시
- Decorator는 Weapon을 상속하며 Weapon을 상속하는 무기에 적용 가능.
- 마법 공격력 같은 경우는 Wand를 상속하는 Decorator 로 만들 수 있을 것.
Weapon : damage, act (버프) 속성 존재. 확장 가능성 있게 만드는 것이 중요.
Wand : Weapon을 상속하는 무기 종류. 기본 데미지만 존재.
Decorator : 각 무기에 특정 효과 주기 위한 데코레이터. 데코레이터 역시 일종의 weapon으로 생각해서 기존 무기 데미지에 속성 데미지를 추가하는 식으로 구현함.
코드의 차이점
act = w.act;
act.Add(SetBuff);
// w.act.Add(SetBuff); 원본에 넣는 방식이라 나쁨
// act = w.act;
using System;
using System.Collections.Generic;
using System.Transactions;
using UnityEngine;
using UnityEngine.PlayerLoop;
abstract class Weapon
{
public float damage = 0f;
public List<Action> act;
}
abstract class Decorator : Weapon
{
// Weapon의 특성을 추가해줄 것이기 때문에 Weapon 상속
public Weapon weapon = null; // 장비의 특성을 더할 때 사용, 없다면 없어도 무방
}
class Wand : Weapon
{
public Wand()
{
damage = 10f;
act = new List<Action>();
}
}
class Fire : Decorator
{
private float fireDamage = 1f;
public Fire(Weapon w)
{
damage += w.damage + fireDamage;
act = w.act;
act.Add(SetBuff);
}
public void SetBuff()
{
Debug.Log("Fire");
}
}
class Ice : Decorator
{
private float iceDamage = 1f;
public Ice(Weapon w)
{
damage += w.damage + iceDamage;
act = w.act; // 원본은 유지
act.Add(SetBuff); //현재 무기에 대해서만 속성 추가
//w.act.Add(SetBuff); // 원본을 바꾸는 방식이라 안 됨
//act = w.act;
}
public void SetBuff()
{
Debug.Log("Ice");
}
}
public class Code : MonoBehaviour
{
void Start()
{
Weapon w = new Wand();
Debug.Log(w.damage);
foreach (var e in w.act)
{
e.Invoke();
}
// ICE 속성 추가
w = new Ice(w);
Debug.Log(w.damage);
foreach (var e in w.act)
{
e.Invoke();
}
// Fire 속성 추가
// w = new Fire(w);
// Debug.Log(w.damage);
//
// foreach (var e in w.act)
// {
// e.Invoke();
// }
}
}
Builder pattern
클래스 변수 = new 클래스로 생성하는 것이 아니라, 한 번 더 생성 함수로 감싸는 느낌
사용하는 이유?
- 클래스 내에 여러 속성이 있을 때 어떤 것은 디폴트 값으로 쓰고 몇개의 속성만 값을 주어 생성하고 싶을 때.
- 함수에 default 값을 사용할 때 default 값을 가지는 것들은 마지막으로 몰아야 함. 그래서 중간에 있는 변수에 디폴트 값을 주려면 빌더 패턴을 사용하는 것이 좋음
Command
예시 - Replay
게임 장면을 다시 돌려서 보는 것. 커맨드 저장(명령 저장)으로 구현 가능. 같은 상황에서 같은 로직, 커맨드를 실행하면 똑같은 결과가 나올 것.
모든 사람의 위치, 사용한 스킬을 저장해서 보여주는 것이 아님. Replay 파일을 보면 용량이 굉장히 작음.
모든 input을 저장함. 즉, 명령을 저장하는 것. 마우스 클릭을 해서 특정 웨이포인트로 캐릭터를 움직였다면 어디를 클릭했는지 정도만 저장함. (실행 시간이 중요한 경우에는 타임스탬프를 저장하기도 한다고 함) 순서대로 어떤 command가 들어왔는지 저장함. 리플레이는 이후 이 command를 가지고 게임을 한 번 더 실행시키는 것. 같은 command가 들어오기 때문에 플레이한 게임과 똑같은 장면 보여줄 수 있음.
Command Pattern
모든 행동의 형식을 지정해 스택에 담아놓는 것. KeyInput, MouseInput …
Command라는 부모 클래스 안에 Undo, Redo, Execute를 구현해 명령어를 취소하거나 실행. Undo stack과 Redo Stack 을 만들어두면 Undo 실행했을 때 Undo 스택에 들어있던 명령어는 Redo로 옮겨지고 Undo 스택의 pointer는 하나 이전에 들어온 대상을 가리킬 것. Redo를 하면 Redo 스택의 위에 있는 명령어가 Undo 스택에 옮겨지고 Redo 스택의 Pointer는 빠져나간 아이템 이전에 들어온 대상을 가리킬 것. undo 한 뒤 새로운 명령어를 실행했다면 redo 스택의 개수를 검사한 다음 redo 스택을 비워주어야 함. 새로운 액션을 실행하면 redo를 할 수 없기 때문.
State
MenuState 예제 (스택으로 구현)
예제 시나리오
RPG UI 창 중 캐릭터 창을 누름 → 강화 버튼 클릭 → 강화 메뉴 창 열림 → 강화 버튼 클릭 → 재화가 부족합니다 띄우고 캐시샵으로 보냄 → 캐시샵 메뉴 창 열림
이 상황에서 닫기를 누르면 바로 직전 화면, 강화 화면이 나와야 함.
스택으로 만들어 특정 화면 보일 때마다 스택에 넣어놓고, 메뉴 닫으면 스택에서 pop 해준다.
Tilemap
Double buffer 사용. 카메라에 보이는 타일맵 일부만 렌더링 해놓음. 캐릭터 속도에 맞추어 Stride (미리 로딩할 타일맵의 개수 or 거리)를 설정해야 함. Stride를 크게 설정해 미리 캐릭터 이동 방향쪽의 타일맵을 로딩해놓거나 Stride를 1로 해놓고 IO를 더 자주 불러 로딩하거나.
Shared Material vs. Material
Mat은 생성할 때마다 같은 texture를 사용해도 바라보는 texture가 다름. Shared Material은 같은 Texture를 가지고 만들었으면 같은 Texture를 바라봄. 메모리적으로 이득.
Material을 그럼 왜 쓰는가? Material마다 서로 다른 Texture option을 주어야 할 때가 있음. 각 Material마다 Texture의 Read/Write 옵션을 설정할 수 있음.
Flyweight
큰 크기의 데이터는 new Data로 생성하기보다는 Scriptable Object 같이 따로 뺀다.
(자료 더 찾아봐야 함)
Prototype
원본을 복사해서 Instantiate하는 것.
Class A = new Class(); 이렇게 만들었을 것
예제 - 몬스터 생성
Demon 클래스를 만들어두고 new Demon()으로 클론함
static은 클래스간에 공유되는 값.
Bytecode
Modding, 외부 확장성을 높이기 위해 사용.
게임 내의 수치를 바꾸는 방법 : UI로 변경 가능하게 해주거나 Modification 기능 제공.
실제 게임을 만들어 프로그래밍을 했으면 각 변수에 특정 값 들어가있을 것. 이 상태에서 complie 하면 유저가 값을 수정할 수 없음. Bytecode를 사용하면 컴파일 이후라도 유저가 값을 변경할 수 있음.
모든 데이터는 bit, Byte로 되어 있음. 유저가 컴파일된 내용을 건드리게 하고 싶다면 데이터를 byte array를 제공해야 함. ByteArray를 Serialize해서 유저에게 보내주고, 유저가 무언가 정보를 바꿔서 다시 ByteArray를 돌려주면 프로그램이 그것을 deserialize해서 게임 내의 정보를 변경할 수 있음.
Photon Custom OP : bytecode로 구현되어 있음.
byte(시작 주소, 길이, 어떤 클래스) : deseriazlie하는 명령어
전략 패턴
Learn #Strategy Pattern in #Unity in Less Than 15 Minutes
Learn Strategy Pattern in Unity in Less Than 15 Mins. Understand how to implement Strategy Pattern in Unity. Quick guide to Strategy Pattern in Unity with Demo.
www.theappguruz.com
아래와 같은 상속 관계가 있다고 해보자. 하위에 있는 것이 자식이다.
무기
도끼 검
한손 양손 한손 양손
Interface로 한손인지 양손인지 지정. 하위 클래스의 공통점을 interface로 빼는 것이 전략 패턴
캐릭터는 한손, 양손의 interface를 상속함.
기능 중심으로 interface를 모두 뽑아놓고 인터페이스를 뽑아다가 쓰는 것이 전략 패턴.
예시
마법에 따라 완드를 차야만 사용할 수 있는 마법이 있고 아닌 마법이 있다고 하자.
- IMagic 이라는 인터페이스를 만들어놓고
- 완드 착용 여부는 각 마법에서 확인.
최대한 범용적으로 만들어 두고 구체적인 부분은 각 클래스에서 확인한다!
종합
생성 관련 패턴 Factory, Builder, Decorator
Strategy pattern
Weapon
Sword Spear
void Start에 코드를 작성한다고 가정했을 때 패턴 사용하지 않고 작성하면
void Start()
{
Character c = new Character();
Weapon w = new Weapon();
}
class Weapon
{
string name;
int damage;
}
Factory 사용하면
public class WeaponFactory
{
public Weapon CreateWeapon(Type wType)
{
return wType == Sword ? new Sword() : new Spear;
}
}
void Start
{
WeaponFactory f = new WeaponFactory();
Weapon w = f.CreateWeapon();
}
Builder 사용하면
public class Weapon
{
//public WeaponType type;
public string name;
public int damage;
public List<Action> action;
public Buff buff;
}
public class WeaponMaker : Weapon
{
public Weapon CreateWeapon(Type wType)
{
return wType == Sword ? new Sword() : new Spear;
}
public WeaponMaker SetDamage(int dmg)
{
this.damage = dmg; // Weapon을 상속하기 때문에 이렇게 쓸 수 있음
return this;
}
}
위에 말한 3가지 패턴을 때려넣으면 다음과 같은 코드가 탄생한다.
using System;
using System.Collections.Generic;
using UnityEngine;
[Flags] // 8자리 안 채워도 됨
public enum Buff // 00000000 2^n 승으로 표현
{
Hp = 0,
Mp = 1,
Fire = 2,
Ice = 4,
Damage = 8,
}
//Strategy pattern 1
public interface IHand
{
public void EquipLeft();
public void EquipRight();
}
public interface IWeaponSkill // Decorator
{
public void Buff();
public void Debuff();
}
public class Weapon
{
//public WeaponType type;
public string name;
public int damage;
public List<Action> action;
public Buff buff;
}
// Weapon이라는 상위 클래스에 IHand를 주지 않고 각 무기에 IHand를 달아주었음.
public class Sword : IHand // Strategy Pattern 2
{
public void EquipLeft()
{
//Equip something
}
public void EquipRight()
{
// Equip something
}
}
public class Spear : IHand // Strategy Pattern 2
{
public void EquipLeft()
{
//Equip something
}
public void EquipRight()
{
Debug.Log("두손무기를 쓰고 있으므로 장착이 불가합니다.");
return;
}
}
public enum WeaponType // Factory
{
Sword,
Spear,
Arrow,
}
public class WeaponMaker : Weapon, IWeaponSkill // Decorator2, Builder
{
private Weapon weapon;
//Builder
public WeaponMaker SetName(string name)
{
this.name = name;
return this;
}
public WeaponMaker SetDamage(int damage)
{
this.damage = damage;
return this;
}
public Weapon Make()
{
return this;
}
public void Buff()
{
}
public void Debuff()
{
}
}
public class BuffSkill : WeaponMaker //Decorator 3
{
public int buffDamage = 10;
public List<Action> action;
// 0~255
// 00000001 ==> 1
// 10000000 ==> 128
// decorater : 기존 weapon damage에 버프 추가
public BuffSkill(Weapon w)
{
buff = gloabl::Buff.Damage;
w.damage += buffDamage;
action = w.action;
action.Add(Buff);
}
public void Buff()
{
Debug.Log("Damage Buff");
}
}
// How to Use
public class Use : MonoBehaviour
{
private Weapon w;
private Weapon w2;
void Start()
{
WeaponMaker wm = new WeaponMaker();
w = wm.SetName("Sword").SetDamage(10).Make(); // Builder사용, 상위 클래스 Weapon으로 받아주었음
w2 = new BuffSkill(w);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
IHand hand = (IHand)w2;
hand.EquipRight(); //w2 라는 무기를 오른손에 장착
}
}
}
후기
그냥 외우기보다는 자주 써서 나만의 패턴 구현법을 만드는 것이 중요할 듯하다.
'Unity 공부' 카테고리의 다른 글
[Unity] Json으로 게임 Player, Stage, Level 데이터 관리 (0) | 2023.01.29 |
---|---|
모바일 게임 성능 최적화 (0) | 2023.01.23 |
[Unity] TMP Text 한글 깨지는 현상 해결 (0) | 2022.12.11 |
[Unity] 최적화 (1) | 2022.12.11 |
[Unity, Flask] Flask로 로컬 서버 구성해 WebGL 빌드 띄워보기 (0) | 2022.12.05 |
댓글