게시글
질문&답변
통일된 Stat을 사용하지 않고 모듈화 방식으로 Stats를 만드신 이유가 궁금합니다.
수강해주셔서 감사합니다.방식에 관해서는 모든 Entity가 통일된 Stat을 가져도 상관은 없습니다.다만, 개발을 하다보면 특정 Entity만 가지고 있는 Stat이나 Player만 가지고 있는 Stat을 만들 때도 존재합니다. Player만 사용할 Stat을 수 십, 수 백 종의 모든 Monster가 가지고 있는 것도 낭비이기 때문에 공용으로 모두 같은 Stat을 가지게 할지, Stat을 따로 선별해서 넣어줄지 선택지를 만들어준 것이라고 생각하시면 됩니다. Stat이라는게 유저의 입장에서 힘, 지능, 민첩 이런 것만 있을 것이라 생각하기 쉽지만, 실제 개발에서는 현금 결제나 광고, 계정에 대한 정보, 게임 내적 Data 등 다양한 것들이 Player의 눈에 보이지 않는 Stat으로 만들어지기도 합니다.저 같은 경우 공용으로 사용할 Stat들을 System에서 설정해두고, Entity가 만들어질 때 자동으로 Entity의 Stats에 설정되게 만들어놓았구요, 거기에 따로 Stat이 필요한 Entity의 경우 직접 필요한 Stat을 추가해주는 방식을 사용합니다.감사합니다.
- 0
- 1
- 8
질문&답변
Effect에 기능 확장에 대해서 질문이 있습니다.
수강해주셔서 감사합니다.Effect를 다양한 상황에 적용시키기 위해서는 EffectAction의 Callback 함수를 연결할 수 있는 다양한 event들을 촘촘히 만들고, 때에 따라서는 특정 Damage 유형을 Stat으로 만드는 등 적절한 처리 방법을 고민해야합니다.예를 들어, 죽으면 즉시 부활하는 효과라고 가정하면 아래와 같이 EffectAction을 만들 수 있을겁니다. EffectAction.cs public override Start(Effect effect) { effect.Owner.Owner.onTakeDamage += OnTakeDamage; } public override bool Apply(Effect effect) { // event를 통해 효과를 적용했다면, 상태를 리셋하고 true를 return if (isApplied) { isApplied = false; return true; } return isApplied; } public override Release(Effect effect) { effect.Owner.Owner.onTakeDamage -= OnTakeDamage; } public void OnTakeDamage(Entity target, float damage); { // 대상이 이번에 Damage를 받고 죽었다면 HP가 1인 상태로 부활시킴 if (target.IsDead) { target.HPStat.Value = 1f; isApplied = true; } }Effect가 실행될 때 Owner Entity의 onTakeDamage Event에 Callback 함수를 연결해두고, Entity가 죽으면 효과가 발동하는 형식입니다.특정 종족에게 가하는 피해 증가는 종족 피해 배율을 Stat으로 만들 수 있을겁니다.예를 들어, Elf 종족에 대한 Damage 증가를 담당하는 INCREASED_DAMAGE_TO_ELF Stat을 만들고, Effect는 Stat의 수치를 증가시키면 됩니다. EffectAction.cs public override Start(Effect effect) { } public override bool Apply(Effect effect) { effect.Owner.Owner.Stats.SetBonusValue("STAT_INCREASED_DAMAGE_TO_ELF", this, 0.2f); } public override Release(Effect effect) { effect.Owner.Owner.Stats.RemoveBonusValue("STAT_INCREASED_DAMAGE_TO_ELF", this); }// Entity.cs public void GiveDamage(Entity target) { float damage = Stats.GetValue("DAMAGE"); if (target.Race == Race.Elf) damage *= Stats.GetValue(STAT_INCREASED_DAMAGE_TO_ELF); target.TakeDamage(damage) }위는 예시라서 if문으로 하드 코딩했지만, 제대로 만들 때는 Entity가 제대로된 종족 값을 가지도록 확장을 해야겠죠.Damage 감소 같은건 Damage를 계산할 때 사용할 Action Callback들을 받아서Invocation을 통해서 연결된 Action들을 순회하면서 최종 값을 구할 수 있습니다 EffectAction.cs public override Start(Effect effect) { effect.Owner.Owner.CalcDamage += CalcDamage; } public override bool Apply(Effect effect) { // event를 통해 효과를 적용했다면, 상태를 리셋하고 true를 return if (isApplied) { isApplied = false; return true; } return isApplied; } public override Release(Effect effect) { effect.Owner.Owner.CalcDamage -= CalcDamage; } private float CalcDamage(Entity instigator, DamageType damageType, float damage) { // 공격한 적의 Damage Type이 화속성이면 Damage를 무묘화시킴. if (damage > 0f && damageType == DamageType.Fire) { isApplied = true; return 0f; } // 적의 Damage Type이 화속성이 아니라면 Effect가 작동하지않음 else return damage; }// TakeDamage 함수 안 // 연결된 Callback 함수들을 순회하며 최종 Damage 계산을 함. // 위 Effect가 발동할 경우 최종 Damage는 0이 됨. foreach (var calc in CalcDamage.GetInvocationList().Cast()) damage = calc.Invoke(this, damageType, damage);보시듯 복잡한 체계를 가진 전투 시스템은 복합적인 방식을 사용하여 구현하여야하기 때문에 모든 프로그래밍적 역량을 끌어올릴 필요가 있습니다. 그래서 RPG 게임 개발이 어렵다고 많이들 얘기하는 것입니다.감사합니다.
- 0
- 2
- 25
질문&답변
공부 방향에 대해 궁금한 점이 있습니다.
수강해주셔서 감사합니다.수강생 분께서는 아직 학습하시는 입장이니 타인의 지식을 받아들여야하는 상태이기 때문에 기억하기 어려운건 당연한 겁니다.프로그래밍이라는게 항상 구조적으로 새로운 무언가를 만들어내는 것이 아닙니다. 내가 공부하고 깨달은 것들을 토대로 자신만의 코딩 스타일이 만들어지면, 그때부터는 여러 게임을 만들어도 구조는 비슷해집니다. 예를 들어, 현재 강의 구조는 QuestSystem - Quest - Task Group - Task인데, 제 다른 강의인 SkillSystem 강의의 경우에는 SkillSystem - Skill - Effect Group - Effect 이런 식으로 만들어집니다. 저는 이미 수 년을 이렇게 프로그래밍 해왔기에 별 생각을 안해도 자연스럽게 이렇게 프로그래밍하게 되고, 코드를 안뜯어봐도 내가 저 시스템을 어떤 구조로 만들었고, 어떤 함수들이 있을지 예상이 됩니다.그리고 사실 개발을 하다보면 보통은 수백 개의 Class, 많게는 수 천 개의 Class 만드는데 개발 기간이 길어지다보면 저도 디테일한 부분을 까먹긴 합니다. 예를 들어, Inventory에 어떤 기능이 필요해서 만들려고 했더니 이미 전에 만들어놨다거나, 함수를 만든 지 오래 되서 세부 기능을 깜빡하고 실수한다거나 사람인 이상 까먹고 실수하기 마련이죠. 솔직히 말씀드리면 개발 아주 초기에 작성한 코드를 다시 보고 제가 왜 이렇게 코드를 짰는지 아리송할 때도 가끔 있습니다. 코드를 건드려보고 왜 그렇게 만들었는지 다시 깨닫게 되죠. 사람 하는 일은 다 똑같고, 현업에 계신 분들, 강의를 만드시는 분들 모두 수강생 분보다 공부한 기간 길고, 경력이 있으니 더 잘할 뿐, 수강생 분보다 인간적으로 뛰어나서 일을 해내고 있는게 아니니 걱정하지 않으셔도 됩니다. 저를 포함한 다른 분들도 모두 수강생 분 같이 생각했던 시기가 있었습니다. 스스로 공부하고 계신 것 자체로 옳은 길을 걷고 계시다고 감히 말씀드리겠습니다.감사합니다.
- 0
- 2
- 43
질문&답변
SkillData 구조체 질문
수강해주셔서 감사합니다.기본적인 이유는 객체 외부에서 Data를 함부로 수정하지 못하게 하기 위함이구요, 부가적으론 Data 객체들은 이름 그대로 별 다른 동작 없이 내부에서만 제어될 Data의 묶음일 뿐이고, 이런 Data 객체들은 struct를 사용하는게 권장되서 그렇습니다.다만, 이는 제 Coding Style일 뿐 절대적인게 아니기 때문에, 판단 하에 class가 낫다고 생각이 드신다면 당연히 class 객체로 바꾸셔도 상관 없습니다.감사합니다.
- 0
- 1
- 45
질문&답변
스킬트리 저장 질문입니다
수강해주셔서 감사합니다.첫번째 방법은 Entity를 DontDestroyObject로 만들어서 동일한 객체를 쭉 가져가는 것입니다.Entity를 재설정하는 것이 번거로울 때 많이 쓰는 방식입니다.두번째 방법은 Skill Tree는 현재 SkillSystem에 등록된 Skill들을 통해 자동으로 복원되므로 결국 SkillSystem을 Save, Load 해주기만 하면 됩니다.SkillSystem을 저장하는 가장 간단한 방법은 json을 이용하는 것입니다.1. 현재 등록된 Skill들의 ID, Level을 Json 형태로 만들어서 PlayerPrefs나 ES3 같은 Save Asset을 통해 저장.2. 저장된 Data가 있을 때 기초 Setup 대신 Load를 진행. Resources.Load로 Skill Database를 불러와 저장했던 Skill들의 id로 Skill들을 찾아오고, Skill들을 SkillSystem에 등록한 뒤, 등록된 Skill들의 Level을 저장했던 Level로 설정해주면 됩니다.Save는 간략히 다음과 같은 형식으로 만들 수 있을거구요,var root = new JObect();var array = new JArray();foreach (var skill in ownedSkills){// skill의 정보를 Json 형태로 만들어 Json 배열에 기록var skillData = new JObject();skillData["id"] = skill.ID;skillData["level"] = skill.Level;array.Add(skillData);}root["skills"] = array;PlayerPrefs.SetString("playerSkills", root.ToString());Load는 간략히 다음과 같이 만들 수 있을겁니다.string savedData = PlayerPrefs.GetString("playerSkills", null);if (savedData == null)// 기초 Setupelsevar root = JObject.Parse(savedData);var array = root["skills"].Value();var database = Resources.Load(path);foreach (JToken token in array){// 저장했던 Json Data를 통해 Database에서 Skill을 찾고, SkillSystem에 등록한 뒤 Level을 복원.var skillData = token.Value();var id = skillData["id"].Value();var level = skillData["level"].Value();var skill = database.datas.Select(x => x.ID == id);var registeredSkill = Register(skill);registeredSkill.Level = level;}만약 Data를 저장할 필요까지는 없다면, 꼭 Json을 안써도 Skill들의 ID와 Level을 변수로 어딘가에 기록해두고 복원해주기만하면 됩니다.둘 중 편한 방식을 골라 사용하시면 될 것 같습니다.(P.S: 오래되서 저도 잊었는데 참고하실만한 비슷한 질문이 있었네요. https://inf.run/4XAay)감사합니다.
- 0
- 2
- 44
질문&답변
BT와 FSM을 활용해 몬스터 AI 구현
수강해주셔서 감사합니다.기본적으론 말씀하신대로가 맞습니다.모든 세부 동작을 FSM으로 구현할 필요는 없구요, FSM은 Skill처럼 동작이 순차적으로 진행되는 Sequence를 이룰 때 활용하면 좋습니다.전체 로직과 단발적 동작들은 BT와 Node가 제어하되, 상태에 대한 제어와 Sequence에 대한 제어는 FSM에 맡기는게 좋습니다.BT├ 조건 (범위 안에 적이 있나?) ─ 조건 (Entity가 Idle 상태인가?) ─ Skill 사용 (SkillSystem.Use)────────────├ Patrol (단발적 동작)────├ 스킬 사용 완료까지 대기감사합니다.
- 0
- 2
- 67
질문&답변
MonoStateMachine을 만든 이유가 잘 이해가 가지 않습니다.
수강해주셔서 감사합니다. StateMachime을 MonoBehaviour를 상속 받아 만들면 비 GameObject 객체에 사용할 수 없습니다. 예를 들어, 강의의 Skill은 SO 객체이므로 Mono가 아닌 일반 클래스로 만들어진 StateMachine이 필요합니다. 개발을 하다보면 비 GameObject에 대한 Logic 제어가 필요한 부분이 꽤 많기 때문에 우선적으로 모든 class에 범용적으로 사용할 수 있도록 일반 class의 StateMachine을 만들어준 뒤, GameObject일 경우 쉽게 StateMachine을 갈아낄 수 있도록 MonoStateMachine을 만들어준 것입니다. 굳이 얘기드리자면 MonoStateMachine을 만들 필요는 없는데 편의성을 위해서 만들어준 것입니다. 감사합니다.
- 0
- 1
- 56
질문&답변
End()와 Complete() 함수를 따로 구현한 의도
수강해주셔서 감사합니다.Complete 함수는 TaskGroup 혹은 Task를 '완료 상태'로 만들고 싶을 때 호출합니다. 예를 들어, '퀘스트 완료권' 아이템을 캐쉬로 판다고 가정했을 때, 이 아이템을 쓰면 TaskGroup의 Complete 함수를 써서 TaskGroup을 완료 상태로 만들어주는 겁니다. 그리고나서 퀘스트를 준 NPC에게 찾아가 말을 걸면 퀘스트가 완료되겠죠.End 함수는 현재 TaskGroup이 완전히 종료되어 더 이상 동작할 필요가 없을 때 호출됩니다. Task가 Complete 된 상태라고 하더라도 설정(CanReceiveReportsDuringCompletion)에 따라서 보고를 계속 받을 수 있던걸 기억하시죠? Complete 함수는 Task를 완료 상태로 만들어줄 뿐, Task를 끝내진(End) 않기 때문에 완료 상태에서 언제든지 미완료 상태로 전환될 수 있습니다. 예를 들어, Task의 조건 아이템(=슬라임 점액 5개)을 먹어서 Task가 완료된 상태(Complete)이지만, 조건 아이템을 다시 버린다면 미완료 상태(Running)로 전환되겠죠? End 함수가 호출되기 전까지는 Task는 완료 여부와 상관 없이 계속 동작 중인 상태인 겁니다.감사합니다.
- 0
- 2
- 65
질문&답변
근접 콤보 공격을 만들고 싶습니다.
수강해주셔서 감사합니다.Skill Combo를 만드는건 강의 마지막에 만든 Skill Tree처럼 xNode를 이용해서 Skill들을 연결하여 발동 순서를 정해주면 될거구요, 당연히 Combo에 사용되는 Skill들은 미리 SkillSystem에 모두 등록이 되어있어야겠죠?Skill Combo가 Q Button에 등록되어있다고 가정하면, Q로 처음 스킬을 발동하면, Q에 등록된 Skill이 다음 Combo Skill로 바뀌고, 특정 타이밍(예를 들어, 현재 발동 중인 스킬이 종료)이 됐을 때 Q를 누르면 바뀐 Combo Skill이 사용되고, Q에 등록된 Skill이 다음 Combo Skill로 변경, 이 과정을 반복하면 됩니다. Combo가 끝났거나 일정 시간안에 Q를 누르지 않으면 Q Button에 등록된 Skill을 초기 Skill로 되돌리면 되겠죠. 만약 Combo가 Level에 따라 점점 확장되는 구조라면, 아직 SkillSystem에 등록되지 않은 Skill이 다음 Combo Skill일 경우에는 바로 Combo를 끝내는 방식으로 만들면 될 것 같습니다.다만, 모든걸 다 Skill로 처리하려고 하실 필요는 없습니다. 만드시려고하는 콤보가 정확히 어떤 방식으로 작동하는 것인진 알 순 없지만, 상황에 따라선 Skill보다는 Combat System을 따로 만드는게 나을 수도 있습니다.감사합니다.
- 0
- 1
- 75
질문&답변
Target의 value에 Object 자료형 관련 질문입니다!
수강해주셔서 감사합니다.public abstract T value { get; }이렇게 만든다는게 class를 TaskTarget 형식으로 만들면 되지 않냐는 말씀이시겠죠?TaskTarget 형식으로 만들면,[SerializeField]private TaskTarget[] targets;이렇게 SerializeField 변수로 받을 수가 없기에 Generic을 쓰지 않는 것입니다.감사합니다.
- 0
- 2
- 65