2023. 2. 11. 19:13ㆍDIY/[개발 중][MSW] 모험가 키우기
익히 알고 있는 방치형 게임의 전투는 자동으로 치러진다.
오늘은 그런 자동 전투를 구현하는 과정을 기록해보겠다.
일단, 몬스터가 죽을 때까지 지속적으로 공격하는 플레이어부터 구현해보자.
그러기 위해선 일정시간마다 적을 공격하는 것을 구현해야 한다.
일단 기본공격을 스킬 시전과 같은 방식으로 수행하겠다.
사거리, 애니메이션 시간, 애니메이션 종류 등등을 고려해야 하지만,
우선은 게임 구실을 하기 위한 정도까지만 구현하겠다.
일단 클래스컴포넌트가 스킬 동작시간을 담을 프라퍼티를 가지고 있다.
이 프라퍼티는 Update문에서 지속적으로 delta값만큼 감소하며
0이 되었을 때 스킬시전이벤트를 만들어 보낸다.
보냄과 동시에 자신의 동작시간 프라퍼티에 방금 시전한 스킬의 동작시간값을 할당한다.
이러면 일정시간마다 자동공격은 일단 완성이 됐다.
이젠 특정한 타이밍 이후부터 자동공격을 수행하도록 해야 한다.
때와 장소를 가리지 않고 공격을 날리면 곤란하기 때문이다.
나는 플레이어가 필드에서 몬스터를 만난 뒤 3초 후부터 공격을 시작하도록 할 것이다.
그러기 위해 BattleManager라는 로직을 만들었다.
그 로직은 전투 상황이 되었을 때에 발생하는 이벤트의 수신대상이 된다.
(ex. 몬스터가 스폰되었을 때?)
그리고 3초나 5초 뒤에 또 다시 자신에게 전투 시작 이벤트를 보낸다.
그러면 플레이어와 몬스터 컴포넌트가 그 이벤트를 구독하고 있다가
자동공격을 수행하기 시작한다.
그러면 이젠 모든 몬스터가 죽었을 때, 전투를 종료하도록 구현해보자.
몬스터가 스폰될 때마다 BattleManager 로직의
enemyNum 프라퍼티를 1씩 증가시킨다.
그리고 몬스터가 죽을 때마다 BattleManager의 OnMonsterDie 함수를 호출한다.
OnMonsterDie 함수는 BattleManager의 enemyNum 프라퍼티를 1씩 감소시킨다.
감소 시켰을 때 0보다 작거나 같다면 BattleManager의 OnBattleEnd 함수가 호출된다.
OnBattleEnd 함수는 OnBattleEndEvent를 자신 로직에 보내는데,
몬스터와 플레이어는 BattleManager에 OnBattleEndEvent가 발생하는 걸
감지하고 자신의 BattleStart 프라퍼티를 false로 변화시켜자동공격을 멈춘다.
1. 동작 애니메이션이 겹쳐서, 몬스터에 의한 피격 애니메이션에
플레이어의 공격 애니메이션이 덮힌다. (완료)
2. 파워 스트라이크 스킬을 쓸 때, 데미지가 들어가는 시점과
실제로 이펙트가 몬스터에게 닿는 시점이 달라 이질감이 든다.
(완료)
3. 몬스터와 플레이어가 서로 공격하는 연출을 구현 해야 한다. (따로 포스팅 예정)
4. 몬스터를 죽게 한 공격도 데미지가 출력돼야 하는데
몬스터가 죽어버려서 그러지 못한다. (완료)
5. 공격함수 자체의 문제점들 (자체 공격함수, 피격함수 구현으로 문제 해결)
1번 해결 과정
문제는 event로 발생하는 플레이어의 공격 동작 애니메이션이
그보다 로우레벨 단에서 발생하는 몬스터에 의한 피격 동작 애니메이션때문에
덮힌다는 것이었다.
처음에는 이 문제를 약간 어렵게 대하고 접근했던 것 같다.
그러나 포럼을 찾아보면서 자연스레 나와 목적이 같은 기능인 슈퍼아머 구현 글을
보게 되었고, 해결 방법은 매우 간단했다.
몬스터에 의한 피격 상태로 전이시키는 컴포넌트인
StateComponent를 아예 비활성화시켜버리는 것이었다.
그리고 피격 동작도 공격 동작과 같은 레벨인 event를 통해 제어하는 것으로
해결하였다.
나는 왜 이 생각을 못했을까?아마 문제를 문장으로 정확히 규명하지 못했고,
그 때문에 문제를 분리하여 접근하지 못했고,
그로 인해 근원적인 해결방법에 접근하지 못했던 것 같다.
문제는 위에서 말했듯
공격 동작 중에 피격되면 피격 동작으로 강제 변경된다는 것이었고,
피격 동작을 없애버리고 싶지만 필요하긴 했다.
그래서 공격 동작도 피격 동작과 같은 레벨단에서 실행시키려는 시도,
공격 동작 중에 피격 동작을 실행시키지 않으려는 시도 등
공격 동작에 초점을 집중했던 것 같다.
그러나 피격 동작을 없애버리고
역으로 피격 동작을 공격 동작과 같은 레벨단에서 실행시킬 생각은 못해봤다.
그것도 아예 stateComponent를 없애버리면서 하는 방법은
생각하지 못했던 방법이어서 신박했다.
피격 동작의 상태 변화 로직을 알면 신박한 방법은 아닐 수 있지만,
그만큼 내 시야가 좁았던 것 같다.
공격 동작 애니메이션을 실행시키는 방법과 동일하게
ActionStateChangedEvent를 이용해서 피격 동작을 발생시킴으로써
해결하였다.
2번 해결 과정
스킬마다 데미지가 출력되어야 할 적절한 타이밍이 다 다르므로
그 타이밍을 직접 손수 설정하여 스킬의 데이터셋에 넣는다.
이렇게 타격할 때 생기는 이질감은 해결됐고,
부가적으로 플레이어의 공격속도를 기준삼아
이펙트 재생속도, 동작애니메이션 재생속도, 데미지 출력 시점을
동기화하였다.
이제는 공격속도가 변화하면 동작 속도도 변화할 것이다.
+동작시간도.
3번 해결 과정
지금의 몬스터는 어떠한 공격 모션도 없이 공격한다.
그래서 플레이어 캐릭터 위에 데미지마저 출력되지 않고, hp도 줄지 않는다면
플레이어는 몬스터가 공격을 하고 있는 건지 아닌 건지 모를 것이다.
그래서 전투한다는 느낌이 날 수 있도록
플레이어는 몬스터를 공격하기 위해 몬스터를 향해 접근하다가
사거리에 닿으면 공격을 해야 하고,
몬스터는 플레이어를 향해 왔다가 플레이어에게 닿으면
공격을 하고 일정 시간 이상 떨어졌다가
다시 와서 때리고 튀는 움직임으로 몬스터의 공격을 연출하겠다.
이를 위해 MovementComponent, AIComponent 등을 알아야 한다.
4번 해결 과정
기존에는 몬스터가 사망에 이르는 피해를 입으면,
그 즉시 소멸하여 데미지 스킨을 출력하지 않았다.
TimerService의 SetTimerOnce로 간단히 해결했다.
Timer의 callback함수로 몬스터가 죽었으니 행동을 멈추라는 의미의 이벤트를 보낸다.
그러면 움직임이나 공격을 하지 않고,
플레이어의 공격 대상에도 포함되지 않은 채 1초동안 대기하다가 소멸된다.
Attack함수 자체의 문제점들
일단 공격 함수의 타겟으로 들어오는 순서의 기준이 무엇인지가 궁금하다.
(분명 패턴은 있음)
(실제로는 스킬의 사거리가 있으므로 그냥 무시 가능)
hitDisplayCount의 정확한 데미지 표기를 위해 공격 함수를 직접 구현해야 할까?
(표기되는 데미지의 오차 범위가 심하게 크지 않으므로 무시 가능)
타수마다 크리티컬 개별 적용을 위해 공격 함수를 직접 구현해야 할까?
-> 구현 필요함
만약 구현을 직접 하게 된다면
attacker측에서는 Attack함수를 구현하는데
Attack함수 호출은 한 번,
OnHit함수 호출도 한 번,
isAttackTarget 연산을 n번(공격 범위에 들어온 엔티티 수만큼 호출됨.
타겟 수를 제한하는 용도의 프라퍼티값을 증가시키는 데 사용됨.)
단, calcDamage 연산을 n번(타수만큼)(각 타수의 명중 여부 연산도 여기서 한다),
calcCritical 연산도 n번(calcDamage 내부에서 호출된다)해서
도출된 데미지 그룹을 OnHit 함수로 넘겨줘야 할 것이다.
defender측에서는 Hit함수를 구현하는데
hit함수 호출은 한 번,
isHitTarget 연산을 한 번,
(이건 calcDamage 내부에서 회피 여부를 연산하는 용도로 호출시켜줘야 할 것 같다.)
calcDamage 연산도 n번 (타수만큼)(각 타수의 회피 여부 연산도 처리) 해야 한다.
그리고 hitEvent에게 데미지 그룹만 넘겨주면 데미지 스킨을 출력할 수 있다.
그러나 OnHit을 사용하지 않을 경우 원하는 데미지 스킨의 종류를 사용할 수 없다.
그래서 OnHit을 그대로 사용할 수는 없다.
OnHit이 원하는 데미지 스킨의 종류를
사용할 수 있는 이유인 로직을 알아내야 한다.
+ 알고보니 간단했다.
hitEvent의 프라퍼티로 attacker 엔티티 정보를 넘겨주면
attacker의 데미지 스킨을 적용시켜 출력시킨다.
따라서 hitEvent를 활용하는데
반드시 nil로 넣으면 안 되는 프라퍼티들은
attackerEntity, damages, totalDamage이다.
이 위의 것들은 반드시 값을 넣어준 후에 이벤트를 보내야 한다.
기존 Attack함수
공격 범위 안의 모든 hit컴포넌트를 가진 엔티티들에게
OnHit 함수 호출시킴.
크리티컬 연산이 한 번만 호출되어 개별 타수마다 적용 불가함.
displayCount의 표기값이 이상함.
원하는 타겟 수까지만 공격이 불가함.
확장 Attack함수의 프로세스
공격 범위 안의 모든 hit컴포넌트를 가진 엔티티들을 취합 후
원하는 타겟 수까지만 OnHit함수 호출시킴.
데미지와 크리티컬 연산을 개별 타수마다 호출하여 원작과 보다 가까움
개별 타수의 데미지를 취합해 그룹으로 담아 보내고
그걸 그대로 데미지 스킨으로 출력시키므로
displayCount의 표기값이 이상할 일이 없음.
그러나
개별타수마다 크리티컬 '표기'를 어떻게 하지?
miss나 guard '표기'를 어떻게 하지?
데미지 스킨 스폰 로직을 정확히 알아 내야 한다.
hitevent로 발생되는 데미지 스킨 스폰의 절차 경로.
크리티컬 데미지 스킨을 출력하는 로직.
답은 DamageSkinService였다.
DamageSkin관련 컴포넌트들이 이 서비스를 이용하고 있었다.
그럼 이제 개별 타수마다
크리티컬 연산을 시키면서도 원작과 유사하게 출력하도록 연출해야 한다.
개별 타수를 구현하는 법은 찾았지만,
각 데미지 스킨들의 가시성이 좋지 않다.
좋으려면 volcano type을 쓰면 그나마 낫긴 하지만,
그러면 원작의 감성이 손실된다.
(일단 단타 공격은 default를 쓰고 다단히트 공격만 volcano로 구현해야겠다.)
문제점을 다시 짚어보자면 여러 타수의 공격일 경우,
데미지 스킨의 크리티컬 표기가 타수만큼 다르게 결정되지 않는다는 문제가 있다.
이는 단타 공격과 다타 공격의 구분이 무의미함을 뜻하고,
디테일과 고찰이 얕아진다는 문제가 생기므로
AttackComponent와 HitComponent의 함수들을 수정하여
이를 개선하려 한다.
일단 HitComponent를 어떤 식으로 만들지 결정해야
AttackComponent를 그에 맞춰 만들 수 있다.
우선, HitComponent에서 한 공격에 포함된 여러 타수의 데미지를
따로 표기시키는 것을 구현해야 한다.
기본 설계 상, 데미지 스킨 한 묶음의 크리티컬 여부는 통일되어있고
무조건 상위 함수에서 결정한 크리티컬 결과를 모두가 따르기 때문이다.
내가 구현한 방식은
Attack함수가 넘겨준 여러 타수의 데미지그룹을 받고,
그 그룹을 순회하며 그룹 요소마다 개별적으로 데미지 스킨을 출력하는 것이다.
(빨간색은 사용되는 파라미터를 뜻함)
이 때, Attack함수가 넘겨준여러 타수의 크리티컬 여부가 담긴 boolean 그룹을
데미지 그룹과 똑같은 방식으로 활용하여
데미지 스킨을 출력할 때 호출하는 함수의 isCritical 인자 칸에 넣어준다.
그럼 이제 각 타수마다 독립적으로 크리티컬 표기를 해주는 모습을 볼 수 있다.
그러나 보는 것과 같이, 데미지 스킨이 겹쳐서 가시성이 떨어지는 문제가 있다.
그래서 데미지 스킨 출력 방식을 화산효과로 바꾸고,
데미지 스킨 간의 출력 간격이 너무 짧은 것 같아
SetTimerRepeat로 0.05의 시간이 흐른 후마다 출력하도록 했다.
그리고 SetTimerOnce로 데미지스킨 출력을 중단하는 함수를
타수 * 0.05의 시간이 흐른 후에 호출되도록 했다.
그러면 위 이미지처럼 적당히 보이게 된다.
HitComponent에서 데미지 스킨을 출력하는 부분은 구현했으니,
AttackComponent와 상호작용하는 나머지 부분을 구현해보자.
기본 함수와 크게 다르지 않다.
위에서 구현한 것들을 작동시키기 위한 인자들을
받아올 수 있도록 몇가지만 수정해주면 된다.
또한 데미지 그룹과 크리티컬 여부 그룹, 타수 정보, 공격 정보 등을 받고
그 정보들과 자신의 방어력 요소를 합쳐 나온 최종 데미지 그룹을
도출해내도록 구현하면 된다.
'DIY > [개발 중][MSW] 모험가 키우기' 카테고리의 다른 글
[MSW] 모험가 키우기 - 8 (스테이지 전환 연출) (0) | 2023.02.24 |
---|---|
[MSW] 모험가 키우기 - 7 (수치 설정, 스킬시스템 개편) (0) | 2023.02.17 |
[MSW] 모험가 키우기 -5 (전투 시스템 - 공격과 피격) (0) | 2023.01.23 |
[MSW] 모험가 키우기 - 4 (스킬시스템 구조 설계) (1) | 2023.01.05 |
[MSW] 모험가 키우기 - 3 (프로토타입을 위한 기본 환경 구성) (0) | 2022.12.26 |