Unity ์‹ฌํ™” - 1

์ด์ค€ํ˜ธยท2023๋…„ 12์›” 26์ผ
0

๐Ÿ“Œ Unity ๊ฒŒ์ž„ ๊ฐœ๋ฐœ ์‹ฌํ™”



๐Ÿ“Œ RPG FSM ํ”„๋กœ์ ํŠธ ์…‹ํŒ…

โž” URP๋ž€?

Unity์˜ Universal Render Pipeline์˜ ์•ฝ์ž๋กœ, ์Šคํฌ๋ฆฝํŠธ ๊ฐ€๋Šฅํ•œ ๋ Œ๋” ํŒŒ์ดํ”„๋ผ์ธ(Scriptable Render Pipeline)์ด๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

  • Cross-Platform Compatibility : URP๋Š” Unity๊ฐ€ ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ๋™์ž‘ํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ๋‹ค. ์ด๋Š” ๋ชจ๋ฐ”์ผ, ๋ฐ์Šคํฌํ†ฑ, ์ฝ˜์†” ๊ฒŒ์ž„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ AR, VR ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—๋„ ์ ํ•ฉํ•˜๋‹ค.

  • Performance and Scalability : URP๋Š” ์„ฑ๋Šฅ ๋ฐ ํ™•์žฅ์„ฑ์„ ๋ชฉํ‘œ๋กœ ์„ค๊ณ„๋˜์—ˆ๋‹ค. ํŠนํžˆ ์ €์‚ฌ์–‘ ์žฅ์น˜์—์„œ๋„ ๋›ฐ์–ด๋‚œ ์„ฑ๋Šฅ์„ ์ œ๊ณตํ•˜๋„๋ก ์ตœ์ ํ™”๋˜์–ด ์žˆ๋‹ค. ๋˜ํ•œ, ๊ทธ๋ž˜ํ”ฝ ์„ค์ •์„ ์‰ฝ๊ฒŒ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด ๋‹ค์–‘ํ•œ ์žฅ์น˜์— ์ ํ•ฉํ•˜๊ฒŒ ์Šค์ผ€์ผ๋ง ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Modern Rendering Features : URP๋Š” ํ˜„๋Œ€์ ์ธ ๋ Œ๋”๋ง ๊ธฐ์ˆ ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด์—๋Š” ์ฃผ์š” ๋ผ์ดํŠธ ์œ ํ˜•, ํ‘œ์ค€ ์‰์ด๋”ฉ ๋ชจ๋ธ, ํ™˜๊ฒฝ ๋ฆฌํ”Œ๋ ‰์…˜ ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.

  • Customizability : URP๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํŠน์ • ๊ฒŒ์ž„ ๋˜๋Š” ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ๊ณ ์œ ํ•œ ๋ Œ๋”๋ง ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Graphics Quality : URP๋Š” ๋†’์€ ํ’ˆ์งˆ์˜ ๊ทธ๋ž˜ํ”ฝ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด๋Š” ํ–ฅ์ƒ๋œ ๋ผ์ดํŠธ ๋ชจ๋ธ, ํ‘œ๋ฉด ์‰์ด๋”ฉ, ํฌ์ŠคํŠธ ํ”„๋กœ์„ธ์‹ฑ ํšจ๊ณผ ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ํœ™๋“๋œ๋‹ค.

  • Simplicity : URP๋Š” Unity์˜ ๊ธฐ์กด ๋ Œ๋”๋ง ์‹œ์Šคํ…œ์— ๋น„ํ•ด ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๋‹ค. ์ด๋Š” ๊ทธ๋ž˜ํ”ฝ ์„ค์ •์„ ๋‹จ์ˆœํ™”ํ•˜๊ณ , ์ดˆ๊ธฐ ์„ค์ •์„ ์‰ฝ๊ฒŒ ํ•˜๋„๋ก ๋„์™€์ฃผ๋Š” ๋„๊ตฌ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.












๐Ÿ“Œ ์ƒํƒœ ๋จธ์‹ (FSM) ์ด๋ž€?

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

์œ ํ•œ ์ƒํƒœ ๋จธ์‹  (FSM)

  • ์œ ํ•œ ์ƒํƒœ ๊ธฐ๊ณ„ (Finite State Machine, FSM)

  • FSM์˜ ๊ฐœ๋…

    • FSM์€ ์œ ํ•œ ์ƒํƒœ ๊ธฐ๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋””์ž์ธ ํŒจํ„ด์ด๋‹ค.
    • ์ƒํƒœ์™€ ์ƒํƒœ ๊ฐ„์˜ ์ „ํ™˜์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๋™์ž‘ ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ์ด๋‹ค.
  • FSM์˜ ๊ตฌ์„ฑ ์š”์†Œ

    • ์ƒํƒœ(State) : ์‹œ์Šคํ…œ์ด ์ทจํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.
    • ์ „ํ™˜ ์กฐ๊ฑด(Transition Condition) : ์ƒํƒœ ๊ฐ„ ์ „ํ™˜์„ ๊ฒฐ์ •ํ•˜๋Š” ์กฐ๊ฑด์ด๋‹ค.
    • ๋™์ž‘(Action) : ์ƒํƒœ์— ๋”ฐ๋ผ ์ˆ˜ํ–‰๋˜๋Š” ๋™์ž‘ ๋˜๋Š” ๋กœ์ง์„ ๋‚˜ํƒœ๋‚œ๋‹ค.
  • FSM์˜ ๋™์ž‘ ์›๋ฆฌ

    • ์ดˆ๊ธฐ ์ƒํƒœ์—์„œ ์‹œ์ž‘ํ•˜์—ฌ ์ž…๋ ฅ ๋˜๋Š” ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ƒํƒœ ์ „ํ™˜์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
    • ์ƒํƒœ ์ „ํ™˜์€ ์ „ํ™˜ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋ฉฐ, ์ „ํ™˜ ์กฐ๊ฑด์€ ์ž…๋ ฅ, ์‹œ๊ฐ„, ์กฐ๊ฑด ๋“ฑ์œผ๋กœ ๊ฒฐ์ •๋œ๋‹ค.
    • ์ƒํƒœ ์ „ํ™˜ ์‹œ ์ด์ „ ์ƒํƒœ์˜ ์ข…๋ฃŒ ๋™์ž‘๊ณผ ์ƒˆ๋กœ์šด ์ƒํƒœ์˜ ์ง„์ž… ๋™์ž‘์ด ์ˆ˜ํ–‰๋œ๋‹ค.
  • FSM์˜ ์žฅ์ 

    • ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•˜๊ณ  ์ƒํƒœ ๊ฐ„ ์ „ํ™˜์„ ์ผ๊ด€๋˜๊ฒŒ ๊ดธ๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ณต์žกํ•œ ๋™์ž‘์„ ์ƒํƒœ์™€ ์ „ํ™˜ ์กฐ๊ฑด์œผ๋กœ ๋‚˜๋ˆ„์–ด ๊ตฌํ˜„ํ•˜๋ฏ€๋กœ ์ฝ”๋“œ ์œ ์ง€ ๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๋‹ค.
    • ๋‹ค์–‘ํ•œ ๋™์ž‘์„ ์œ ๊ธฐ์ ์œผ๋กœ ์กฐํ•ฉํ•˜์—ฌ ์›ํ•˜๋Š” ๋™์ž‘์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • FSM์˜ ์˜ˆ์‹œ : ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ ๊ด€๋ฆฌ

    • ์ƒํƒœ : ์ •์ง€ ์ƒํƒœ, ์ด๋™ ์ƒํƒœ, ์ ํ”„ ์ƒํƒœ
    • ์ „ํ™˜ ์กฐ๊ฑด : ์ด๋™ ์ž…๋ ฅ, ์ ํ”„ ์ž…๋ ฅ, ์ถฉ๋Œ ๋“ฑ์˜ ์กฐ๊ฑด
    • ๋™์ž‘ : ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์žฌ์ƒ, ์ ํ”„ ์ฒ˜๋ฆฌ, ์ด๋™ ์†๋„ ์กฐ์ • ๋“ฑ











๐Ÿ“Œ ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ๋จธ์‹  ์ค€๋น„

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

ํ”„๋กœ๋นŒ๋”(ProBuilder)

ํ”„๋กœ๋นŒ๋”(ProBuilder)๋Š” Unity์˜ ์œ ์šฉํ•œ ์—์…‹ ์ค‘ ํ•˜๋‚˜๋กœ, ๊ฒŒ์ž„ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋น ๋ฅด๊ณ  ์‰ฝ๊ฒŒ 3D ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ํŽธ์ง‘ํ•˜๋Š” ๋„๊ตฌ์ด๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ Unity์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์œผ๋กœ, ์œ ๋ฃŒ ๋ผ์ด์„ผ์Šค๊ฐ€ ํ•„์š”์—†์ด ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ”„๋กœ๋นŒ๋”์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๊ณผ ํŠน์ง•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • 3D ๋ชจ๋ธ๋ง : ํ”„๋กœ๋นŒ๋”๋Š” ๋‚ด์žฅ๋œ ๋„๊ตฌ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ 3D๋ชจ๋ธ์„ ์‰ฝ๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ ๊ธฐํ•˜ ๋„ํ˜•(์ •์œก๋ฉด์ฒด, ์›๊ธฐ๋‘ฅ ๋“ฑ)์„ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ชจ๋ธ๋ง ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋” ๋ณต์žกํ•œ ๋ชจ๋ธ์€ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

  • ํŽธ์ง‘๊ณผ ์ˆ˜์ • : ํ”„๋กœ๋นŒ๋”๋Š” ๊ฐ์ฒด๋ฅผ ์„ ํƒํ•˜๊ณ  ์ด๋™, ํšŒ์ „ ์Šค์ผ€์ผ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํŽธ์ง‘ ๋„๊ตฌ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค. ๋˜ํ•œ ์ •์ , ์—ฃ์ง€, ๋ฉด ์ˆ˜์ค€์—์„œ ์„ธ๋ถ€์ ์ธ ํŽธ์ง‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

  • UV ๋งคํ•‘ : 3D๋ชจ๋ธ์˜ ํ…์Šค์ฒ˜ UV๋งคํ•‘์„ ํ”„๋กœ๋นŒ๋”์—์„œ ์ง์ ‘ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ…์Šค์ฒ˜๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ฝœ๋ฆฌ์ „ ์„ค์ • : ํ”„๋กœ๋นŒ๋”๋Š” ๊ฐ„๋‹จํ•œ ์ฝœ๋ฆฌ์ „ ๋ฉ”์‰ฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๊ฒŒ์ž„ ์บ๋ฆญํ„ฐ๋‚˜ ๊ฐ์ฒด์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”๋ฐ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์„ฑ๋Šฅ ์ตœ์ ํ™” : ํ”„๋กœ๋นŒ๋”๋Š” ๋‹ค๋ฅธ ๋ชจ๋ธ๋ง ๋„๊ตฌ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์˜ ํด๋ฆฌ๊ณค ๊ฐœ์ˆ˜๋ฅผ ์ตœ์ ํ™”ํ•˜๊ณ  ๊ฒŒ์ž„ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.




์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ(Character Controller)

์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ(Character Controller)๋Š” Unity์—์„œ ์บ๋ฆญํ„ฐ๋‚˜ ํ”Œ๋ ˆ์ด์–ด์˜ ์›€์ง์ž„๊ณผ ์ถฉ๋Œ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฌผ๋ฆฌ ์—”์ง„์ด ์•„๋‹Œ ์บ๋ฆญํ„ฐ์˜ ์›€์ง์ž„์„ ํ”„๋ ˆ์ž„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ, ์ฃผ๋กœ 3D์บ๋ฆญํ„ฐ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๊ณผ ํŠน์ง•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ์บ๋ฆญํ„ฐ ์ด๋™ : ์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋‹จ์ˆœํ•œ ์ด๋™์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์ฃผ๋กœ ์ด๋™ ๋ฐฉํ–ฅ๊ณผ ์ด๋™ ์†๋ ฅ์„ ์„ค์ •ํ•˜์—ฌ ์บ๋ฆญํ„ฐ๋ฅผ ์›€์ง์ด๊ฒŒ ํ•œ๋‹ค.

  • ์ค‘๋ ฅ ์ ์šฉ : ์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์ค‘๋ ฅ์„ ์ ์šฉํ•˜์—ฌ ์ ํ”„๋‚˜ ๋–จ์–ด์ง์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ถฉ๋Œ ์ฒ˜๋ฆฌ : ์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋ฌผ๋ฆฌ ์—”์ง„์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ์บ๋ฆญํ„ฐ์˜ ์ถฉ๋Œ์„ ๊ฐ์ง€ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋ฅธ ์ฝœ๋ฆฌ๋”์™€์˜ ์ถฉ๋Œ์„ ํ†ต์ œํ•˜๊ณ , ๊ฒฝ์‚ฌ๋กœ์™€์˜ ์ƒํ˜ธ์ž‘์šฉ ๋“ฑ์„ ์ง€์›ํ•œ๋‹ค.

  • ๋ฐ”๋‹ฅ ๊ฒ€์ถœ : ์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์บ๋ฆญํ„ฐ๊ฐ€ ๋ฐ”๋‹ฅ ์œ„์— ๋†“์ด๋„๋ก ๋ฐ”๋‹ฅ ๊ฒ€์ถœ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. ๋ฐ”๋‹ฅ๊ณผ์˜ ๊ฑฐ๋ฆฌ, ํ‘œ๋ฉด ๋…ธ๋ฉ€ ๋“ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ์บ๋ฆญํ„ฐ์˜ ๋†’์ด๋ฅผ ์กฐ์ ˆํ•˜๊ฑฐ๋‚˜ ์ ํ”„๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

  • ์›€์ง์ž„ ์ œํ•œ : ์บ๋ฆญํ„ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์›€์ง์ž„์„ ์ œํ•œํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์ œ๊ณตํ•œ๋‹ค. ์ง€์ •๋œ ์˜์—ญ ๋‚ด์—์„œ๋งŒ ์›€์ง์ด๋„๋ก ํ•˜๊ฑฐ๋‚˜, ์ง€ํ˜•์˜ ๊ฒฝ์‚ฌ๋ฅผ ๋”ฐ๋ผ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.






โž” ํ”„๋กœ์ ํŠธ ์ค€๋น„

ํŒจํ‚ค์ง€ ์„ค์น˜

  • Input System
  • Cinemachine
  • Pro Builder





โž” ์ง€ํ˜• ๋งŒ๋“ค๊ธฐ

ํ”„๋กœ ๋นŒ๋” ์‚ฌ์šฉํ•˜๊ธฐ

  • Tools - ProBuilder - ProBuilder Window

  • New Shape ํด๋ฆญ - Pro Builder Shape Size (50 0 50)

  • ํฌํ•จ๋œ Texture01 ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž

  • ๋ฉด๊ณผ ์„ ์„ ์„ ํƒํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ชจ์–‘์˜ ์ง€ํ˜•์„ ์ค€๋น„.
    ํ๋ธŒ์™€ ์•„์น˜๋ฅผ ์ถ”๊ฐ€.

  • Extrude Faces ๋ฉด ์ƒ์„ฑ






โž” ํ”Œ๋ ˆ์ด์–ด ๋งŒ๋“ค๊ธฐ

์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ

  • ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ - Player ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • Paladin ๋ชจ๋ธ ์ถ”๊ฐ€

ํ…์Šค์ฒ˜ ์ˆ˜์ •ํ•˜๊ธฐ

  • Window - Rendering - Render Pipeline Converter ํด๋ฆญ

  • ์ „์ฒด ์„ ํƒ - Convert ํด๋ฆญ

Input Action ๋งŒ๋“ค๊ธฐ

  • Input Action ์ƒ์„ฑ

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •

  • Input Actions - Generate C# Class ์ฒดํฌ - Apply






โž” StateMachine ์ค€๋น„ํ•˜๊ธฐ

PlayerInput.cs ์ž‘์„ฑ

public class PlayerInput : MonoBehaviour
{
    public PlayerInputActions InputActions { get; private set; }
    public PlayerInputActions.PlayerActions PlayerActions { get; private set; }

    private void Awake()
    {
        InputActions = new PlayerInputActions();

        PlayerActions = InputActions.Player;
    }

    private void OnEnable()
    {
        InputActions.Enable();
    }

    private void OnDisable()
    {
        InputActions.Disable();
    }
}

PlayerAnimationData.cs ์ž‘์„ฑ

[Serializable]
public class PlayerAnimationData
{
    [SerializeField] private string groundParameterName = "@Ground";
    [SerializeField] private string idleParameterName = "Idle";
    [SerializeField] private string walkParameterName = "Walk";
    [SerializeField] private string runParameterName = "Run";

    [SerializeField] private string airParameterName = "@Air";
    [SerializeField] private string jumpParameterName = "Jump";
    [SerializeField] private string fallParameterName = "Fall";

    [SerializeField] private string attackParameterName = "@Attack";
    [SerializeField] private string comboAttackParameterName = "ComboAttack";


    // ๋•…์— ์žˆ๋‹ค.
    public int GroundParameterHash { get; private set; }
    public int IdleParameterHash { get; private set; }
    public int WalkParameterHash { get; private set; }
    public int RunParameterHash { get; private set; }

    // ๊ณต์ค‘์— ์žˆ๋‹ค.
    public int AirParameterHash { get; private set; }
    public int JumpParameterHash { get; private set; }
    public int fallParameterHash { get; private set; }

    // ๊ณต๊ฒฉ์ค‘์ด๋‹ค.
    public int AttackParameterHash { get; private set; }
    public int ComboAttackParameterHash { get; private set; }

    // ํ•ด์‹œ ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ
    public void Initialize()
    {
        GroundParameterHash = Animator.StringToHash(groundParameterName);
        IdleParameterHash = Animator.StringToHash(idleParameterName);
        WalkParameterHash = Animator.StringToHash(walkParameterName);
        RunParameterHash = Animator.StringToHash(runParameterName);

        AirParameterHash = Animator.StringToHash(airParameterName);
        JumpParameterHash = Animator.StringToHash(jumpParameterName);
        fallParameterHash = Animator.StringToHash(fallParameterName);

        AttackParameterHash = Animator.StringToHash(attackParameterName);
        ComboAttackParameterHash = Animator.StringToHash(comboAttackParameterName);
    }
}

Player.cs ์ž‘์„ฑ

public class Player : MonoBehaviour
{
    // AnimationAdata๋ฅผ Header์— ์—ฐ๊ฒฐ
    [field: Header("Animations")]
    [field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }

    public Rigidbody Rigidbody { get; private set; }
    public Animator Animator { get; private set; }
    public PlayerInput Input { get; private set; }
    public CharacterController Controller { get; private set; }

    private void Awake()
    {
        // AnimationData ์ดˆ๊ธฐํ™” ์ง„ํ–‰
        AnimationData.Initialize();

        Rigidbody = GetComponent<Rigidbody>();
        Animator = GetComponentInChildren<Animator>();
        Input = GetComponent<PlayerInput>();
        Controller = GetComponent<CharacterController>();
    }

    private void Start()
    {
        // ์ฒ˜์Œ ์‹œ์ž‘์‹œ Cursor์— Lock์„ ๊ฑธ์–ด์ค€๋‹ค.
        Cursor.lockState = CursorLockMode.Locked;
    }
}



ํ”Œ๋ ˆ์ด์–ด ์˜ค๋ธŒ์ ํŠธ ์„ค์ •

  • Player ์˜ค๋ธŒ์ ํŠธ - Player.cs ์ถ”๊ฐ€



IState.cs ์ž‘์„ฑ

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}

StateMachine.cs ์ž‘์„ฑ

public abstract class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();   // ์ด ์ „ ์ƒํƒœ๋ฅผ ๋งˆ๋ฌด๋ฆฌ
        currentState = newState;    // ์ƒˆ๋กœ์šด ์ƒํƒœ ์ ์šฉ
        currentState.Enter();   // ๋‹ค์‹œ ์‹œ์ž‘
    }

    // ํ˜„์žฌ State์˜ Handle์„ ์‹คํ–‰
    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    // ํ˜„์žฌ State์˜ Update() ์‹คํ–‰
    public void Update()
    {
        currentState?.Update();
    }

    public void PhysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}











๐Ÿ“Œ ํ”Œ๋ ˆ์ด์–ด ์Šคํ…Œ์ดํŠธ ๋จธ์‹  ๋งŒ๋“ค๊ธฐ

โž” Player Script Object ์ค€๋น„

PlayerGroundData.cs ์ž‘์„ฑ

[Serializable]
public class PlayerGroundData   // ํšŒ์ „, ์ด๋™์˜ ์ฒ˜๋ฆฌ
{
    [field: SerializeField][field: Range(0f, 25f)] public float BaseSpeed { get; private set; } = 5f;
    [field: SerializeField][field: Range(0f, 25f)] public float BaseRotationDamping { get; private set; } = 1f;

    [field: Header("IdleData")]

    [field: Header("WalkData")]
    [field: SerializeField][field: Range(0f, 2f)] public float WalkSpeedModifier { get; private set; } = 0.225f;

    [field: Header("RunData")]
    [field: SerializeField][field: Range(0f, 2f)] public float RunSpeedModifier { get; private set; } = 1f;
}

PlayerAirData.cs ์ž‘์„ฑ

[Serializable]
public class PlayerAirData // ์ ํ”„ ๋ฐ์ดํ„ฐ
{
    [field: Header("JumpData")]
    [field: SerializeField][field: Range(0f, 25f)] public float JumpForce { get; private set; } = 4f;
}

PlayerSO.cs ์ž‘์„ฑ

[CreateAssetMenu(fileName = "Player", menuName = "Characters/Player")]
public class PlayerSO : ScriptableObject
{
    [field: SerializeField] public PlayerGroundData GroundData { get; private set; }
    [field: SerializeField] public PlayerAirData AirData { get; private set; }
}

Data ์ƒ์„ฑ

  • Create๋ฉ”๋‰ด๋ฅผ ํ†ตํ•ด PlayerSO์ƒ์„ฑ

  • ์•„๋ž˜์•„ ๊ฐ™์ด ์„ค์ •






โž” Player Ground State ๊ตฌ์„ฑ

PlayerBaseState.cs ์ž‘์„ฑ

public class PlayerBaseState : IState
{
    protected PlayerStateMachine stateMachine;
    protected readonly PlayerGroundData groundData;

    // ์—ญ์ฐธ์กฐ
    public PlayerBaseState(PlayerStateMachine playerStateMachine)
    {
        stateMachine = playerStateMachine;
        groundData = stateMachine.Player.Data.GroundData;
    }

    public virtual void Enter()
    {
        AddInputActionsCallbacks();
    }

    public virtual void Exit()
    {
        RemoveInputActionsCallbacks();
    }

    public virtual void HandleInput()
    {
        ReadMovementInput();
    }

    public virtual void PhysicsUpdate()
    {
        
    }

    public virtual void Update()
    {
        Move();
    }

    // InputActionํ•œํ…Œ Callback์„ ๊ฑธ์–ด์ค„ ๊ฒƒ์ด๋‹ค. (Evnet๋ฅผ ๊ฑธ์–ด์ฃผ๋˜ ๋Š๋‚Œ)
    protected virtual void AddInputActionsCallbacks()
    {

    }

    protected virtual void RemoveInputActionsCallbacks()
    {

    }

    private void ReadMovementInput()
    {
        // ์—ญ์ฐธ์กฐ & ์บ์‹ฑ (์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์• ๋“ค์€ ๋ฏธ๋ฆฌ ์บ์‹ฑ์„ ํ•ด๋‘๋Š”๊ฒŒ ์ข‹๋‹ค)
        stateMachine.MovementInput = stateMachine.Player.Input.PlayerActions.Movement.ReadValue<Vector2>();
    }

    // ์‹ค์ œ ์ด๋™์ฒ˜๋ฆฌ
    private void Move()
    {
        Vector3 movementDirection = GetMovementDirection();

        Rotate(movementDirection);

        Move(movementDirection);
    }

    // ์นด๋ฉ”๋ผ๊ฐ€ ๋ฐ”๋ผ๋ณด๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™, ํ•˜๊ธฐ์œ„ํ•œ ๋ฐฉํ–ฅ
    private Vector3 GetMovementDirection()
    {
        Vector3 forward = stateMachine.MainCameraTransform.forward; // ๋ฉ”์ธ ์นด๋ฉ”๋ผ๊ฐ€ ๋ฐ”๋ผ๋ณด๋Š” ์ •๋ฉด
        Vector3 right = stateMachine.MainCameraTransform.right; // ์˜ค๋ฅธ์ชฝ์„ ๋ฐ›์•„์˜จ๋‹ค.

        forward.y = 0;  // y๊ฐ’์„ ์ œ๊ฑฐํ•ด์•ผ ๋•…๋ฐ”๋‹ฅ์„ ๋ณด๊ณ  ๊ฐ€์ง€ ์•Š๋Š”๋‹ค.
        right.y = 0;

        // Normalize : Vector ์ž์ฒด๋ฅผ Normalizeํ•œ๋‹ค.
        // Normalized : Vector๋ฅผ Normalizeํ•ด์„œ ๊ฐ€์ง€๊ณ ์˜จ๋‹ค.
        forward.Normalize();
        right.Normalize();

        // ๊ฐ๊ฐ ์ด๋™ํ•ด์•ผํ•˜๋Š” Vector. ์•ž์œผ๋กœ๊ฐ€๋Š” Vector, ์šฐ์ธก์œผ๋กœ ๊ฐ€๋Š” Vector ์—๋‹ค ์ž…๋ ฅํ•œ
        // ์ด๋™ ๋ฐฉํ–ฅ์˜ ๊ฐ’์„ ๊ณฑํ•ด์„œ ์ด๋™
        return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x;
    }

    private void Move(Vector3 movementDiraction)
    {
        float movementSpeed = GetMovementSpeed();
        stateMachine.Player.Controller.Move((movementDiraction * movementSpeed) * Time.deltaTime);  // ์ด๋™ ๋ฐฉํ–ฅ * ์ด๋™ ์Šคํ”ผ๋“œ
    }

    private void Rotate(Vector3 movementDirection)
    {
        if (movementDirection != Vector3.zero)  // ์ž…๋ ฅ์ด ๋์„ ๋•Œ, (zero๊ฐ€ ์•„๋‹ ๋•Œ)
        {
            Transform playerTransform = stateMachine.Player.transform;
            Quaternion targetRotation = Quaternion.LookRotation(movementDirection); // ()์•ˆ์˜ ๋ฐฉํ–ฅ์„ ๋ฐ”๋ผ๋ณด๋Š” Quaternion
            playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);   // Slerp : ํƒ€์›ํ˜• ๋ณด๊ฐ„        
        }
    }

    private float GetMovementSpeed()
    {
        float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;  // ์‹ค์ œ๋กœ ์ด๋™ํ•ด์•ผ ํ•˜๋Š” ์†๋„
        return movementSpeed;
    }

    // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋“ค
    protected void StartAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, true);
    }

    protected void StopAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, false);
    }
}

PlayerGroundedState.cs ์ž‘์„ฑ

public class PlayerGroundState : PlayerBaseState
{
    public PlayerGroundState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        base.Enter();
        // Ground์— ์žˆ์œผ๋ฉด GroundState๋ผ๋Š” bool๊ฐ’์ด ์ผœ์ ธ์žˆ์„ ๊ฒƒ์ด๋ผ Ground์ƒํƒœ์ธ์ง€ ๊ฒ€์‚ฌ ๊ฐ€๋Šฅ
        StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
    }

    public override void Update()
    {
        base.Update();
    }

    public override void PhysicsUpdate()
    {
        base.PhysicsUpdate();
    }
}

PlayerIdleState.cs ์ž‘์„ฑ

public class PlayerIdleState : PlayerGroundState // ๋•…์— ์žˆ์„ ๋•Œ, Idle์„ ํ•  ํ…Œ๋‹ˆ Ground์ƒ์†
{
    public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 0f;    // Idle์ƒํƒœ์— ์›€์ง์ž„์„ ๋ฏธ์—ฐ์— ๋ฐฉ์ง€
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Update()
    {
        base.Update();
    }
}

PlayerStateMachine.cs ์ž‘์„ฑ

public class PlayerStateMachine : StateMachine
{
    public Player Player { get; }

    // States
    public PlayerIdleState idleState { get; }

    public Vector2 MovementInput { get; set; }  // Input ์ž…๋ ฅ๊ฐ’ Vector
    public float MovementSpeed { get; private set; }    // ์ด๋™ ์†๋„
    public float RotationDamping { get; private set; }  // ํšŒ์ „ ๋Œํ•‘
    public float MovementSpeedModifier { get; set; } = 1f;  // ์ด๋™ ๊ด€๋ จ ๊ณฑํ•˜๊ธฐ ๊ฐ’

    public float JumpForce { get; set; }

    public Transform MainCameraTransform { get; set; }

    // ์ƒ์„ฑ์ž
    public PlayerStateMachine(Player player)
    {
        this.Player = player;

        idleState = new PlayerIdleState(this);

        MainCameraTransform = Camera.main.transform;

        MovementSpeed = player.Data.GroundData.BaseSpeed;
        RotationDamping = player.Data.GroundData.BaseRotationDamping;
    }
}





โž” Player ์„ค์ •

Player.cs ์ˆ˜์ •

''' ์ƒ๋žต
// ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ์ค€๋น„
[field: Header("References")]
[field: SerializeField] public PlayerSO Data { get; private set; }

''' ์ƒ๋žต
private void Awake()
{
    // AnimationData ์ดˆ๊ธฐํ™” ์ง„ํ–‰
    AnimationData.Initialize();

    Rigidbody = GetComponent<Rigidbody>();
    Animator = GetComponentInChildren<Animator>();
    Input = GetComponent<PlayerInput>();
    Controller = GetComponent<CharacterController>();
    stateMachine = new PlayerStateMachine(this);    // PlayerStateMachine ์ƒ์„ฑ์ž์—๊ฒŒ this(์ž๊ธฐ์ž์‹ .Player)๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค.
}

private void Start()
{
    // ์ฒ˜์Œ ์‹œ์ž‘์‹œ Cursor์— Lock์„ ๊ฑธ์–ด์ค€๋‹ค.
    Cursor.lockState = CursorLockMode.Locked;
    stateMachine.ChangeState(stateMachine.idleState);
}

private void Update()
{
    stateMachine.HandleInput();
    stateMachine.Update();
}

private void FixedUpdate()
{
    stateMachine.PhysicsUpdate();
}
''' ์ƒ๋žต

์˜ค๋ธŒ์ ํŠธ ์„ค์ •

  • PlayerSO ์—ฐ๊ฒฐ

  • Player Input ์ถ”๊ฐ€

  • Character Controller ์ถ”๊ฐ€

Animator ์„ค์ •

  • Animator Controller ์ƒ์„ฑ

  • ํ”Œ๋ ˆ์ด์–ด ๋ชจ๋ธ - Animator ์ถ”๊ฐ€ - Animator Controller ์—ฐ๊ฒฐ

Animator Controller ์„ค์ •

  • Sub Sub-Machine ์ถ”๊ฐ€ - Ground๋กœ ๋ณ€๊ฒฝ

  • Parameters ์ถ”๊ฐ€

  • Transition ์„ค์ •

  • ๊ฐ ์ƒํ™ฉ์— ๋งž๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ True, False๋ฅผ ์กฐ์ ˆ
    Has Exit Time ์ฒดํฌ ํ•ด์ œ











๐Ÿ“Œ ํ”Œ๋ ˆ์ด์–ด ์ด๋™ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

โž” ์ด๋™ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

PlayerBaseState.cs ์ˆ˜์ •

'''์ƒ๋žต
// InputActionํ•œํ…Œ Callback์„ ๊ฑธ์–ด์ค„ ๊ฒƒ์ด๋‹ค. (Evnet๋ฅผ ๊ฑธ์–ด์ฃผ๋˜ ๋Š๋‚Œ)
protected virtual void AddInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled += OnMoveCanceled;    // .canceled : ํ‚ค๊ฐ€ ๋–ผ์–ด์กŒ์„ ๋•Œ
    input.PlayerActions.Run.started += OnRunStarted;
}

protected virtual void RemoveInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled -= OnMoveCanceled;    
    input.PlayerActions.Run.started -= OnRunStarted;
}

protected virtual void OnMoveCanceled(InputAction.CallbackContext context)
{
        
}

protected virtual void OnRunStarted(InputAction.CallbackContext context)
{
        
}
'''์ƒ๋žต





PlayerGroundState.cs ์ˆ˜์ •

''' ์ƒ๋žต
protected override void OnMoveCanceled(InputAction.CallbackContext context)
{
    if (stateMachine.MovementInput == Vector2.zero) // ์ž…๋ ฅ์ด ์—†์œผ๋ฉด
    {
        return;
    }

    // ์ด๋™ํ‚ค๊ฐ€ ๋–ผ์–ด์กŒ์„ ๋•Œ, ์ด๊ฑธ ์™œ Ground์—์„œ ๋งŒ๋“œ๋ƒ?
    // ๊ณต์ค‘(Air)์ƒํƒœ || ๊ตฌ๋ฅผ ๋•Œ, ๋“ฑ Ground๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ State์— ์žˆ์„ ๋•Œ, ํ‚ค๋ฅผ ๋–ผ๋ฉด ๋‹ค๋ฅธ ๋™์ž‘์„
    // ํ•ด์•ผํ•œ๋‹ค. Idle๋กœ ๋„˜์–ด์˜ค๋ฉด ์•ˆ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ๋™์ž‘์€ Ground๊ฐ€ ๋ณด๋Š”๊ฒŒ ๋งž๋‹ค.
    // Ground์ผ ๋•Œ, ํ‚ค๋ฅผ ๋–ผ๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ๊ฒƒ์ธ๊ฐ€? ์— ๋Œ€ํ•œ ๊ฒƒ์ด๋‹ˆ๊นŒ.
    stateMachine.ChangeState(stateMachine.IdleState);

    base.OnMoveCanceled(context);
}

protected virtual void OnMove()
{
    // ์ด๋™์ด ์ผ์–ด๋‚˜๋ฉด Walk๋กœ ์˜ฎ๊ฒจ์ค€๋‹ค.
    stateMachine.ChangeState(stateMachine.WalkState);
}

PlayerWalkState.cs ์ž‘์„ฑ

public class PlayerWalkState : PlayerGroundState
{
    public PlayerWalkState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = groundData.WalkSpeedModifier;  // Walk์˜ ์†๋„๋กœ ๊ณฑํ•ด์ฃผ๋Š” ๊ฐ’
        // ๋ถ€๋ชจ PlayerGroundState๊ฐ€ ๊ฐ–๊ณ ์žˆ๋Š” Enter()๋ฅผ ์‹คํ–‰ํ•ด GroundํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ‚ค๊ณ 
        // ๋‹ค์‹œ ๋Œ์•„์™€์„œ WalkํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ผœ์ค€๋‹ค. ๊ทธ๋Ÿฌ๋ฉด WalkState์— ๋“ค์–ด์™”์œผ๋ฉด
        // Ground, Walk ๋‘๊ฐœ์˜ bool๊ฐ’์ด ์ผœ์ง„๋‹ค.
        base.Enter();   
        StartAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
    }

    // Walk(๊ฑท๋‹ค๊ฐ€) Run์„ ๋ˆ„๋ฅธ๋‹ค? ๊ทธ๋Ÿผ Run์œผ๋กœ ์ „ํ™˜
    protected override void OnRunStarted(InputAction.CallbackContext context)
    {
        base.OnRunStarted(context);
        stateMachine.ChangeState(stateMachine.RunState);
    }
}

PlayerRunState.cs ์ž‘์„ฑ

public class PlayerRunState : PlayerGroundState
{
    public PlayerRunState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    // ์ด๋™์ฒ˜๋ฆฌ ๋“ค์€ Base์—์„œ ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ ์†๋„๋งŒ ์ฒ˜๋ฆฌ
    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = groundData.RunSpeedModifier;   // Run ์†๋„๋กœ
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.RunParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.RunParameterHash);
    }
}





PlayerStateMachine.cs ์ˆ˜์ •

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStateMachine : StateMachine
{
''' ์ƒ๋žต
    // States
    public PlayerIdleState IdleState { get; }
    public PlayerWalkState WalkState { get; }
    public PlayerRunState RunState { get; }

''' ์ƒ๋žต

    public PlayerStateMachine(Player player)
    {
        this.Player = player;

        IdleState = new PlayerIdleState(this);
        WalkState = new PlayerWalkState(this);
        RunState = new PlayerRunState(this);

        MainCameraTransform = Camera.main.transform;

        MovementSpeed = player.Data.GroundedData.BaseSpeed;
        RotationDamping = player.Data.GroundedData.BaseRotationDamping;
    }
}

PlayerIdleState.cs ์ˆ˜์ •

''' ์ƒ๋žต

public override void Update()
{
    base.Update();

    if (stateMachine.MovementInput != Vector2.zero) // ์ด๋™์ด ์ผ์–ด๋‚ฌ๋‹ค๋ฉด?
    {
        OnMove();   // OnMove() : Idle์—์„œ Walk๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์ฒ˜๋ฆฌ
        return;
    }
}











๐Ÿ“Œ ์‹œ๋„ค๋จธ์‹  ์นด๋ฉ”๋ผ ์ ์šฉ

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

์‹œ๋„ค๋จธ์‹ (Cinemachine)

์‹œ๋„ค๋จธ์‹ (Cinemachine)์€ Unity์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ณ ๊ธ‰ ์นด๋ฉ”๋ผ ์‹œ์Šคํ…œ์ด๋‹ค. ์ด ์‹œ์Šคํ…œ์€ ๊ฒŒ์ž„, ์˜ํ™”, ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋“ฑ์—์„œ ๋‹ค์–‘ํ•œ ์นด๋ฉ”๋ผ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ์นด๋ฉ”๋ผ ์ด๋™, ์ถ”์ , ๋งคํ•‘, ๋ธ”๋ Œ๋”ฉ ๋“ฑ์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค. ์‹œ๋„ค๋จธ์‹ ์€ Unity 2017 ๋ฒ„์ „ ์ดํ›„๋ถ€ํ„ฐ ๊ธฐ๋ณธ์ ์œผ๋กœ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, ์œ ๋‹ˆํ‹ฐ ์—์…‹ ์Šคํ† ์–ด์—์„œ๋„ ์ถ”๊ฐ€์ ์ธ ํ™•์žฅ ํŒจํ‚ค์ง€๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

์‹œ๋„ค๋จธ์‹ ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๊ณผ ํŠน์ง•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • ๊ฐ€์ƒ ์นด๋ฉ”๋ผ(Virtual Camera) : ์‹œ๋„ค๋จธ์‹ ์€ ๊ฐ€์ƒ ์นด๋ฉ”๋ผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ์นด๋ฉ”๋ผ๊ฐ€ ์—†๋Š” ์ƒํƒœ์—์„œ๋„ ์—ฌ๋Ÿฌ ๊ฐ€์ƒ ์นด๋ฉ”๋ผ์˜ ์‹œ์ ๊ณผ ์†์„ฑ์„ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ ๊ฐ€์ƒ ์นด๋ฉ”๋ผ๋Š” ๋…๋ฆฝ์ ์ธ ์นด๋ฉ”๋ผ ์†์„ฑ์„ ๊ฐ€์ง€๋ฉฐ, ์›ํ•˜๋Š” ์ˆœ๊ฐ„์— ํ™œ์„ฑํ™” ๋˜๊ฑฐ๋‚˜ ๋น„ํ™œ์„ฑํ™”๋  ์ˆ˜ ์žˆ๋‹ค.

  • ์นด๋ฉ”๋ผ ๋ธ”๋ Œ๋”ฉ : ์—ฌ๋Ÿฌ ๊ฐ€์ƒ ์นด๋ฉ”๋ผ ๊ฐ„์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ „ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ๋ธ”๋ Œ๋”ฉ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์นด๋ฉ”๋ผ์˜ ์œ„์น˜, ํšŒ์ „,์‹œ์•ผ ๋“ฑ์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ „ํ™˜๋˜์–ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์นด๋ฉ”๋ผ ์ด๋™์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํŠธ๋ž™๋ง๊ณผ ์ถ”์  : ์‹œ๋„ค๋จธ์‹ ์€ ์˜ค๋ธŒ์ ํŠธ๋‚˜ ์บ๋ฆญํ„ฐ๋ฅผ ์ถ”์ ํ•˜์—ฌ ์ž๋™์œผ๋กœ ์นด๋ฉ”๋ผ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋„๋ก ์ง€์›ํ•œ๋‹ค. ํŠน์ • ํƒ€๊ฒŸ์„ ์ถ”์ ํ•˜๊ฑฐ๋‚˜ ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ์ด๋™ํ•˜๋Š” ์นด๋ฉ”๋ผ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์—ฐ๋™ : ์‹œ๋„ค๋จธ์‹ ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์Šคํ…œ๊ณผ ์—ฐ๋™ํ•˜์—ฌ ์นด๋ฉ”๋ผ์˜ ์†์„ฑ์„ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋™๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋ Œ์ฆˆ ์„ธํŠธ : ๋ Œ์ฆˆ ์„ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ Œ์ฆˆ ์กฐ์ž‘์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์คŒ, ํ™”๊ฐ, ํŠน์ˆ˜ ๋ Œ์ฆˆ ํšจ๊ณผ ๋“ฑ์„ ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.






โž” ์‹œ๋„ค๋จธ์‹  ์ถ”๊ฐ€ํ•˜๊ธฐ

Virtual Camera ์ถ”๊ฐ€ํ•˜๊ธฐ

  • Create - Cinemachine - Virtual Camera ํด๋ฆญ

  • CameraLookPoint ์ถ”๊ฐ€

  • Virtual Camera ์ˆ˜์ •

  • Body, Aim ์ˆ˜์ •

  • Add Extension โž” Cinemachine Collider ์ถ”๊ฐ€











๐Ÿ“Œ ํ”Œ๋ ˆ์ด์–ด ์ ํ”„ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

โž” ํž˜ ์ ์šฉํ•˜๊ธฐ

ForceReceiver.cs ๋งŒ๋“ค๊ธฐ

public class ForceReciver : MonoBehaviour
{
    [SerializeField] private CharacterController controller;
    [SerializeField] private float drag = 0.3f;

    private Vector3 dampingVelocity;
    private Vector3 impact;
    private float verticalVelocity;

    // impact : ์ถ”๊ฐ€์ ์ธ ํž˜์„ ๋ฐ›์œผ๋ฉด? ๊ทธ ํž˜์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ impacte
    public Vector3 Movement => impact + Vector3.up * verticalVelocity; // verticalVelocity : ์ˆ˜์ง์˜ ํž˜

    private void Update()
    {
        if (verticalVelocity < 0f && controller.isGrounded) // ๋•…์— ๋ถ™์–ด์žˆ๋Š”๊ฐ€?
        {
            verticalVelocity = Physics.gravity.y * Time.deltaTime; // gravity : -9.7 ?
        }
        else // ๋•…์ด ์•„๋‹ˆ๋ผ๋ฉด?
        {
            verticalVelocity += Physics.gravity.y * Time.deltaTime; // ๊ฐ€์†๋„ ์ฒ˜๋ฆฌ
        }

        // SmoothDamp : current => target ๊นŒ์ง€ ๊ฐ’์˜ ๋ถ€๋“œ๋Ÿฌ์šด ๋ณ€ํ™”
        impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);
    }

    // ๋–จ์–ด์ง€๋Š” ํž˜ ์ดˆ๊ธฐํ™”
    public void Reset()
    {
        impact = Vector3.zero;
        verticalVelocity = 0f;
    }

    public void AddForce(Vector3 force)
    {
        impact += force;
    }

    public void Jump(float jumpForce)
    {
        verticalVelocity += jumpForce;
    }
}

Player.cs ์ˆ˜์ •

''' ์ƒ๋žต
public ForceReceiver ForceReceiver { get; private set; }

''' ์ƒ๋žต
ForceReceiver = GetComponent<ForceReceiver>();

PlayerBaseState.cs ์ˆ˜์ •

''' ์ƒ๋žต

private void Move(Vector3 movementDiraction)
{
    float movementSpeed = GetMovementSpeed();
    stateMachine.Player.Controller.Move(((movementDiraction * movementSpeed) + stateMachine.Player.ForceReciver.Movement) * Time.deltaTime);  // ์ด๋™ ๋ฐฉํ–ฅ * ์ด๋™ ์Šคํ”ผ๋“œ + ForceReciver
}





โž” ์ ํ”„ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

PlayerBaseState.cs ์ˆ˜์ •

''' ์ƒ๋žต
protected virtual void AddInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled += OnMovementCanceled;
    input.PlayerActions.Run.started += OnRunStarted;

    stateMachine.Player.Input.PlayerActions.Jump.started += OnJumpStarted;
}

protected virtual void RemoveInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled -= OnMovementCanceled;
    input.PlayerActions.Run.started -= OnRunStarted;

    stateMachine.Player.Input.PlayerActions.Jump.started -= OnJumpStarted;
}

protected virtual void OnJumpStarted(InputAction.CallbackContext context)
{

}
''' ์ƒ๋žต

PlayerGroundState.cs ์ˆ˜์ •

''' ์ƒ๋žต
protected override void OnJumpStarted(InputAction.CallbackContext context)
{
    // ์ ํ”„ ํ‚ค๊ฐ€ ๋ˆŒ๋ฆฌ๋ฉด JumpState๋กœ ๋ณ€๊ฒฝ (GroundState์•ˆ์ด๋‹ˆ ๋•…์— ๋ถ™์–ด์žˆ๋Š” ์ƒํƒœ์ด๋‹ค.)
    stateMachine.ChangeState(stateMachine.JumpState);
}





PlayerAirState.cs ๋งŒ๋“ค๊ธฐ

public class PlayerAirState : PlayerBaseState
{
    public PlayerAirState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.AirParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.AirParameterHash);
    }
}

PlayerJumpState.cs ๋งŒ๋“ค๊ธฐ

public class PlayerJumpState : PlayerAirState
{
    public PlayerJumpState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.JumpForce = stateMachine.Player.Data.AirData.JumpForce;    // SO์— ์„ค์ •ํ•œ ์ ํ”„ ๊ฐ’ ๋งŒํผ
        stateMachine.Player.ForceReciver.Jump(stateMachine.JumpForce);  // ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋Œ€์ž…

        base.Enter();

        StartAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
    }

    public override void Exit()
    {
        base.Exit();

        StopAnimation(stateMachine.Player.AnimationData.JumpParameterHash);
    }

    public override void PhysicsUpdate()
    {
        base.PhysicsUpdate();

        // velocity.y๊ฐ€ 0๋ณด๋‹ค ์ž‘์•„์ € ์Œ์ˆ˜๊ฐ€ ๋˜์–ด ๋–จ์–ด์ง€๋Š” ์‹œ์ ์ด ๋  ๋•Œ
        if (stateMachine.Player.Controller.velocity.y <= 0)
        {
            stateMachine.ChangeState(stateMachine.FallState);   // ๋–จ์–ด์ง€๋Š”(Fall)๋กœ ์ „ํ™˜
            return;
        }
    }
}

PlayerFallState.cs ๋งŒ๋“ค๊ธฐ

public class PlayerFallState : PlayerAirState
{
    public PlayerFallState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        base.Enter();

        StartAnimation(stateMachine.Player.AnimationData.fallParameterHash);
    }

    public override void Exit() 
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.fallParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (stateMachine.Player.Controller.isGrounded)
        {
            stateMachine.ChangeState(stateMachine.IdleState);
            return;    
        }
    }
}

PlayerStateMachine.cs ์ˆ˜์ •

'''์ƒ๋žต
public PlayerJumpState JumpState { get; }
public PlayerFallState FallState { get; }

'''์ƒ๋žต
JumpState = new PlayerJumpState(this);
FallState = new PlayerFallState(this);





Animator Controller ์ˆ˜์ •

  • Air Sub-State ์ถ”๊ฐ€ โž” @Air ํŒŒ๋ผ๋ฉ”ํ„ฐ๋ฅผ ์ด์šฉํ•ด Transition ๋งŒ๋“ค๊ธฐ

  • Ground๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ํŒŒ๋ผ๋ฉ”ํ„ฐ๋ฅผ ๋งž์ถฐ Transition ๋งŒ๋“ค๊ธฐ

โž” ์ถ”๋ฝ ๋งŒ๋“ค๊ธฐ

PlayerGroundState.cs ์ˆ˜์ •

''' ์ƒ๋žต
public override void PhysicsUpdate()
{
    base.PhysicsUpdate();

    // ๋•…์ด ์•„๋‹ ๋•Œ && velocity.y ๊ฐ’์ด Physics.gravity.y * Time.fixedDeltaTime ๋ณด๋‹ค ์ž‘์„ ๋•Œ
    // Physics.gravity.y * Time.fixedDeltaTime : gravity๋ฅผ ํ•œ ํƒ€์ž„์— ์ ์šฉํ•˜๋Š” ๊ฐ’ , ํ›จ์”ฌ ๋” ๋น ๋ฅธ์†๋„๋กœ ๋–จ์–ด์ง€๊ณ  ์žˆ๋‹ค๋ฉด
    // ์ฆ‰, ๋•…์ด ์•„๋‹ˆ๊ณ  ๋–จ์–ด์ง€๊ณ  ์žˆ๋‹ค๋ฉด FallState๋กœ ๋ฐ”๊ฟ”๋ผ
    if (!stateMachine.Player.Controller.isGrounded && stateMachine.Player.Controller.velocity.y < Physics.gravity.y * Time.fixedDeltaTime)
    {
       stateMachine.ChangeState(stateMachine.FallState);
       return;
    }
}











๐Ÿ“Œ ํ”Œ๋ ˆ์ด์–ด ๊ณต๊ฒฉ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

โž” ๊ณต๊ฒฉ ์ƒํƒœ ์ค€๋น„ํ•˜๊ธฐ

AttackInfoData.cs ๋งŒ๋“ค๊ธฐ

[Serializable]
public class AttackInfoData
{
    [field: SerializeField] public string AttackName { get; private set; } // ๊ณต๊ฒฉ์˜ ์ด๋ฆ„
    [field: SerializeField] public int ComboStateIndex { get; private set; } // ์ฝค๋ณด ์ƒํƒœ์˜ ์ธ๋ฑ์Šค
    [field: SerializeField][field: Range(0f, 1f)] public float ComboTransitionTime { get; private set; } // ์ฝค๋ณด๊ฐ€ ์œ ์ง€๋˜๋Š” ์‹œ๊ฐ„
    [field: SerializeField][field: Range(0f, 3f)] public float ForceTransitionTime { get; private set; } // ๊ณต๊ฒฉ์„ ๋ˆ„๋ฅด๋Š” ์‹œ๊ฐ„
    [field: SerializeField][field: Range(-10f, 10f)] public float Force { get; private set; } // ํž˜์„ ์–ธ์ œ ํด๋ฆญํ•˜๋ฉด ์–ธ์ œ ์ ์šฉํ• ๊ฑด์ง€

    [field: SerializeField] public int Damage { get; private set; }
}

[Serializable]
public class PlayerAttackData
{
    [field: SerializeField] public List<AttackInfoData> AttackInfoDatas { get; private set; } // ์ฝค๋ณด์— ๋Œ€ํ•œ ์ข…๋ฅ˜๋ฅผ ๊ฐ€์ง
    public int GetAttackInfoCount() { return AttackInfoDatas.Count;} // AttackInfo์— ๋Œ€ํ•œ Count๋ฅผ ๊ฐ€์ ธ์˜ด. ๋ช‡๊ฐœ์ธ์ง€
    public AttackInfoData GetAttackInfo(int index) {  return AttackInfoDatas[index]; } // index๋ฅผ ๋Œ€์ž…ํ•˜๋ฉด ํ˜„์žฌ ์‚ฌ์šฉํ•˜๊ณ ์žˆ๋Š” AttackData์— ๋Œ€ํ•ด์„œ ๊ฐ€์ ธ์˜ด 
}

PlayerSO ์ˆ˜์ •

'''์ƒ๋žต
[field: SerializeField] public PlayerAttackData AttakData { get; private set; }

Animator Controller ์ˆ˜์ •

  • Attack Sub-State ์ถ”๊ฐ€, Transition ์ถ”๊ฐ€ (@Attack)

  • 1Attack โž” 2Attack : Combo Equals 1
    2Attack โž” 3Attack : Combo Equals 2


    Entry โž” 2Attack : ComboAttack True, Combo Equals 1
    Enrty โž” 3Attack : ComboAttack True, Combo Equals 2


    *\โž”Exit : ComboAttack False





โž” ๊ณต๊ฒฉ ์ƒํƒœ ๋งŒ๋“ค๊ธฐ

PlayerAttackState.cs ๋งŒ๋“ค๊ธฐ

public class PlayerAttackState : PlayerBaseState
{
    public PlayerAttackState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
    	stateMachine.MovementSpeedModifier = 0; // ๊ณต๊ฒฉ์ค‘์— ์›€์ง์ด์ง€ ์•Š๊ฒŒ
        base.Enter();

        StartAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
    }
}

PlayerStateMachine.cs ์ˆ˜์ •

'''์ƒ๋žต
public PlayerComboAttackState ComboAttackState { get; }

'''์ƒ๋žต
public bool IsAttacking { get; set; }
public int ComboIndex { get; set; }

public PlayerStateMachine(Player player)
{
	'''์ƒ๋žต
	ComboAttackState = new PlayerComboAttackState(this);

PlayerBaseState.cs ์ˆ˜์ •

''' ์ƒ๋žต
protected void ForceMove()
{
   // ์œ„ Move()์™€ ๋‹ฌ๋ฆฌ Direction๊ฐ’(๋งค๊ฐœ๋ณ€์ˆ˜)๋ฅผ ๋ฐ›์ง€์•Š๊ณ  ForceReciver๊ฐ€ ๊ฐ–๊ณ ์žˆ๋Š” Movemenet๊ฐ’์œผ๋กœ๋งŒ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋งŒ๋“ค์–ด์กŒ์„ ๋ฟ์ด๋‹ค stateMachine.Player.Controller.Move(stateMachine.Player.ForceReceiver.Movement * Time.deltaTime);
}

''' ์ƒ๋žต
// InputActionํ•œํ…Œ Callback์„ ๊ฑธ์–ด์ค„ ๊ฒƒ์ด๋‹ค. (Evnet๋ฅผ ๊ฑธ์–ด์ฃผ๋˜ ๋Š๋‚Œ)
protected virtual void AddInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled += OnMoveCanceled;    // .canceled : ํ‚ค๊ฐ€ ๋–ผ์–ด์กŒ์„ ๋•Œ
    input.PlayerActions.Run.started += OnRunStarted;

    stateMachine.Player.Input.PlayerActions.Jump.started += OnJumpStarted;

    // ์ง€๊ธˆ์€ ๊ณต๊ฒฉ, ์ ํ”„, ๋‹ฌ๋ฆฌ๊ธฐ, ์ด๋™ ๋“ฑ ์ „๋ถ€ ๊ณต์šฉ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํ‚ค ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ์‹์œผ๋กœ
    // ๋งŒ๋“ค์–ด ๋‘์—ˆ์ง€๋งŒ, ๋‚˜์ค‘์— ํŠน์ • ์ƒํƒœ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ํ‚ค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค๊ณ  ํ•˜๋ฉด, ์—ฌ๊ธฐ์„œ ๊ณ ์šฉ์œผ๋กœ
    // ๋งŒ๋“ค๊ธฐ ๋ณด๋‹ค๋Š” ๊ฐ ์ƒํ™ฉ์˜ ํฐ ์ƒํƒœ๋“ค, Ground, Ataack, Air ๋“ฑ ๊ทธ๊ณณ์—์„œ overrideํ•ด์„œ ๊ตฌํ˜„ํ•˜๋Š”
    // ๊ฒƒ์ด ํ›จ์”ฌ ๋” ๊น”๋”ํ•  ๊ฒƒ์ด๋‹ค.
    // ์ง€๊ธˆ์€ ๊ณต์šฉ์ด๋‹ˆ๊นŒ.-> ๊ณต๊ฒฉ๋ฒ„ํŠผ์„ ๊ณต์ค‘์—์„œ๋„ ์“ฐ๊ณ , ์ ํ”„์ค‘์—์„œ๋„ ์“ฐ๊ณ  ํ•  ์ˆ˜์žˆ๋Š” ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ
    stateMachine.Player.Input.PlayerActions.Attack.performed += OnAttackPerformed;
    stateMachine.Player.Input.PlayerActions.Attack.canceled += OnAttackCanceled;
}

protected virtual void RemoveInputActionsCallbacks()
{
    PlayerInput input = stateMachine.Player.Input;
    input.PlayerActions.Movement.canceled -= OnMovementCanceled;
    input.PlayerActions.Run.started -= OnRunStarted;

    stateMachine.Player.Input.PlayerActions.Jump.started -= OnJumpStarted;

    stateMachine.Player.Input.PlayerActions.Attack.performed -= OnAttackPerformed;
    stateMachine.Player.Input.PlayerActions.Attack.canceled -= OnAttackCanceled;
}

// Performed : ๋ˆŒ๋ ค์ง€๊ณ  ์žˆ๋Š” ๋™์•ˆ์—๋Š” true
protected virtual void OnAttackPerformed(InputAction.CallbackContext obj)
{
    stateMachine.IsAttacking = true;
}

// Cancel : ๋–ผ๋ฉด false
protected virtual void OnAttackCanceled(InputAction.CallbackContext obj)
{
    stateMachine.IsAttacking = false;
}

'''์ƒ๋žต
protected float GetNormalizedTime(Animator animator, string tag)
{
    AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0); // GetCurrentAnimatorStateInfo : ํ˜„์žฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •๋ณด
    AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0); // GetNextAnimatorStateInfo : ๋‹ค์Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •๋ณด

    // IsInTransition : ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์€ animator๊ฐ€ ๋ผ์ธ์„ ํƒ€๊ณ ์žˆ๋‚˜?
    // nextInfo.IsTag(tag) : ๋‹ค์Œ tag๊ฐ€ Attack(๋งค๊ฐœ๋ณ€์ˆ˜ tag)์ธ๊ฐ€?
    if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
    {
        // ๋ธ”๋žœ์ง€๋“œ ๋˜์–ด์„œ ์„ž์ด๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ ํŠธ๋žœ์ง€์…˜์„ ํƒ”์œผ๋ฉด ๋‹ค์Œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ๋งž๋‹ค.
        return nextInfo.normalizedTime; // ๋‹ค์Œ๊บผ์— ๋Œ€ํ•œ normalizedTime์„ ๋„˜๊ฒจ์คŒ
    }
    else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag)) // currentInfo.IsTag(tag) : ๋‚˜์˜ ํƒœ๊ทธ๊ฐ€ Attack์ด๋ƒ?
    {
        return currentInfo.normalizedTime; // ํ˜„์žฌ๋ฅผ ๋„˜๊ฒจ์คŒ
    }
    else
    {
        return 0; // ๋‘˜ ๋‹ค ์•„๋‹ˆ๋ฉด Attack์ด ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ. 0์„ return;
    }
}





PlayerComboAttack.cs ๋งŒ๋“ค๊ธฐ

public class PlayerComboAttackState : PlayerAttackState
{
    private bool alreadyAppliedForce; // ์ฝค๋ณด๋ฅผ ์ ์šฉ ํ–ˆ๋Š”์ง€
    private bool alreadyApplyCombo; // ํฌ์Šค๋ฅผ ์ ์šฉ ํ–ˆ๋Š”์ง€

    AttackInfoData attackInfoData;

    public PlayerComboAttackState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        base.Enter();

        StartAnimation(stateMachine.Player.AnimationData.ComboAttackParameterHash);

        // ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฐ’์˜ ์ดˆ๊ธฐํ™”
        alreadyAppliedForce = false;
        alreadyApplyCombo = false;

        int comboIndex = stateMachine.ComboIndex;
        attackInfoData = stateMachine.Player.Data.AttackData.GetAttackInfo(comboIndex); // ํ˜„์žฌ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” comboIndex์— ๋Œ€ํ•œ ComboAttack์˜ ์ •๋ณด
        stateMachine.Player.Animator.SetInteger("Combo", comboIndex);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.ComboAttackParameterHash);

        // ์ฝค๋ณด๋ฅผ ์ ์šฉํ•˜์ง€ ๋ชปํ•œ ์ƒํƒœ๋กœ ์ด State๊ฐ€ ๋๋‚ฌ๋‹ค๋ฉด, ๊ณต๊ฒฉ์ค‘์— ์ฝค๋ณด๋ฅผ ์„ฑ๊ณตํ•˜์ง€ ์•Š์•—๋‹ค๋Š” ๊ฒƒ
        // ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ฝค๋ณด์ •๋ณด๋ฅผ 0์œผ๋กœ ๋Œ๋ ค๋†”์•ผ ๋‹ค์Œ์— ๋‹ค์‹œ ๊ณต๊ฒฉํ•  ๋•Œ, ์ฒซ ์ฝค๋ณด๊ณต๊ฒฉ๋ถ€ํ„ฐ ๋‚˜๊ฐ„๋‹ค.
        if (!alreadyApplyCombo)
            stateMachine.ComboIndex = 0;    // ์ฝค๋ณด๋ฅผ ์ ์šฉํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด, 0์œผ๋กœ ๋งŒ๋“ ๋‹ค.
    }

    private void TryComboAttack()
    {
        if (alreadyApplyCombo) return;  // ์ฝค๋ณด์–ดํƒ์„ ์ด๋ฏธ ํ–ˆ๋‹ค๋ฉด?(true๋ผ๋ฉด) return;

        if (attackInfoData.ComboStateIndex == -1) return; // Index๊ฐ€ -1 ? ์ฆ‰ ๋งˆ์ง€๋ง‰ ๊ณต๊ฒฉ ๊ทธ๋Ÿผ return;

        if (!stateMachine.IsAttacking) return; // !IsAttacking ? ๊ณต๊ฒฉ์ด ๋๋‚ฌ๋‹ค๋Š” ๊ฒƒ, ๊ทธ๋Ÿผ return;

        alreadyApplyCombo = true; // ์œ„์— ๋ชจ๋“  ์กฐ๊ฑด์ด ์•„๋‹ˆ๋ผ ํ•˜๋ฉด? true
    }

    private void TryApplyForce()
    {
        if (alreadyAppliedForce) return;    // ์ด๋ฏธ ์ ์šฉํ•œ ์ ์ด ์žˆ๋‹ค? return;
        alreadyAppliedForce = true; // ์•„๋‹ˆ๋ฉด true;

        stateMachine.Player.ForceReciver.Reset();   // ๊ฐ–๊ณ ์žˆ๋˜ ํž˜. Force๋ฅผ Reset

        // AddForce() : ๋‚ด๊ฐ€ ๋ฐ”๋ผ๋ณด๊ณ ์žˆ๋Š” ์ •๋ฉด์—์„œ ๋ฐ€๋ ค๋‚˜๊ฒŒ
        stateMachine.Player.ForceReciver.AddForce(stateMachine.Player.transform.forward * attackInfoData.Force);
    }

    public override void Update()
    {
        base.Update();

        ForceMove();

        // animator์˜ tag๊ฐ’์„ ๋„˜๊ฒจ ํ•ด๋‹น ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ normalizedTime์„ ๊ฐ€์ ธ์˜จ๋‹ค.
        float normalizedTime = GetNormalizedTime(stateMachine.Player.Animator, "Attack"); 
        if (normalizedTime < 1f)    // 1๋ณด๋‹ค ์ž‘๋‹ค? ๊ทธ๋Ÿผ ์•„์ง ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง„ํ–‰์ค‘
        {
            if (normalizedTime >= attackInfoData.ForceTransitionTime)
                TryApplyForce(); // ํž˜ ์ ์šฉ

            if (normalizedTime >= attackInfoData.ComboTransitionTime)
                TryComboAttack(); // ์ฝค๋ณด ์ ์šฉ
        }
        else // ์•„๋‹ˆ๋‹ค? ์ฒ˜๋ฆฌ ๋๋‚ฌ์„ ๋•Œ
        {
            if (alreadyApplyCombo)  // ์ฝค๋ณด๊ฐ€ ์ง„ํ–‰์ค‘? ๋‹ค์Œ ์ฝค๋ณด๋กœ ๋ณ€๊ฒฝ
            {
                stateMachine.ComboIndex = attackInfoData.ComboStateIndex;
                stateMachine.ChangeState(stateMachine.ComboAttackState);
            }
            else
            {
                stateMachine.ChangeState(stateMachine.IdleState);
            }
        }
    }
}

PlayerGroundState.cs ์ˆ˜์ •

''' ์ƒ๋žต
public override void Update()
{
    base.Update();

    if (stateMachine.IsAttacking) // IsAttacking ์ด true๋ผ๋ฉด?
    {
        OnAttack(); // ์‹คํ–‰
        return;
    }
}











๐Ÿ“Œ ์  ๋งŒ๋“ค๊ธฐ

Player์™€ ๊ฑฐ์˜ ์œ ์‚ฌํ•˜๋‹ค.

โž” ์  ์ƒํƒœ ๋จธ์‹  ์ค€๋น„ํ•˜๊ธฐ

EnemyStateMachine.cs ๋งŒ๋“ค๊ธฐ

public class EnemyStateMachine : StateMachine
{
    public Enemy Enemy { get; }

    public Transform Target { get; private set; }

    public EnemyIdleState IdlingState { get; }
    public EnemyChasingState ChasingState { get; }
    public EnemyAttackState AttackState { get; }

    public Vector2 MovementInput { get; set; }
    public float MovementSpeed { get; private set; }
    public float RotationDamping { get; private set; }
    public float MovementSpeedModifier { get; set; } = 1f;

    public EnemyStateMachine(Enemy enemy)
    {
        Enemy = enemy;
        Target = GameObject.FindGameObjectWithTag("Player").transform;

        IdlingState = new EnemyIdleState(this);
        ChasingState = new EnemyChasingState(this);
        AttackState = new EnemyAttackState(this);

        MovementSpeed = enemy.Data.GroundedData.BaseSpeed;
        RotationDamping = enemy.Data.GroundedData.BaseRotationDamping;
    }
}

Enemy.cs ๋งŒ๋“ค๊ธฐ

public class Enemy : MonoBehaviour
{
    [field: Header("References")]
    [field: SerializeField] public EnemySO Data { get; private set; }

    [field: Header("Animations")]
    [field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }

    public Rigidbody Rigidbody { get; private set; }
    public Animator Animator { get; private set; }
    public ForceReciver ForceReceiver { get; private set; }
    public CharacterController Controller { get; private set; }

    private EnemyStateMachine stateMachine;

    void Awake()
    {
        AnimationData.Initialize();

        Rigidbody = GetComponent<Rigidbody>();
        Animator = GetComponentInChildren<Animator>();
        Controller = GetComponent<CharacterController>();
        ForceReceiver = GetComponent<ForceReciver>();

        stateMachine = new EnemyStateMachine(this);
    }

    private void Start()
    {
        stateMachine.ChangeState(stateMachine.IdlingState);
    }

    private void Update()
    {
        stateMachine.HandleInput();

        stateMachine.Update();
    }

    private void FixedUpdate()
    {
        stateMachine.PhysicsUpdate();
    }
}



EnemySO.cs ๋งŒ๋“ค๊ธฐ

[CreateAssetMenu(fileName = "EnemySO", menuName = "Characters/Enemy")]
public class EnemySO : ScriptableObject
{
    [field: SerializeField] public float PlayerChasingRange { get; private set; } = 10f;
    [field: SerializeField] public float AttackRange { get; private set; } = 1.5f;
    [field: SerializeField][field: Range(0f, 3f)] public float ForceTransitionTime { get; private set; }
    [field: SerializeField][field: Range(-10f, 10f)] public float Force { get; private set; }
    [field: SerializeField] public int Damage { get; private set; }
    [field: SerializeField][field: Range(0f, 1f)] public float Dealing_Start_TransitionTime { get; private set; }
    [field: SerializeField][field: Range(0f, 1f)] public float Dealing_End_TransitionTime { get; private set; }

    [field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
}





โž” ์  ์ƒํƒœ ์ ์šฉํ•˜๊ธฐ

EnemyBaseState.cs ๋งŒ๋“ค๊ธฐ

// Player์™€ ๊ฑฐ์˜ ๋™์ผ
public class EnemyBaseState : IState
{
    protected EnemyStateMachine stateMachine;

    protected readonly PlayerGroundData groundData;
    public EnemyBaseState(EnemyStateMachine ememyStateMachine)
    {
        stateMachine = ememyStateMachine;
        groundData = stateMachine.Enemy.Data.GroundedData;
    }

    public virtual void Enter()
    {

    }

    public virtual void Exit()
    {

    }

    public virtual void HandleInput()
    {

    }

    public virtual void Update()
    {
        Move();
    }

    public virtual void PhysicsUpdate()
    {

    }

    protected void StartAnimation(int animationHash)
    {
        stateMachine.Enemy.Animator.SetBool(animationHash, true);
    }

    protected void StopAnimation(int animationHash)
    {
        stateMachine.Enemy.Animator.SetBool(animationHash, false);
    }

    private void Move()
    {
        Vector3 movementDirection = GetMovementDirection();

        Rotate(movementDirection);
        Move(movementDirection);
    }

    protected void ForceMove()
    {
        stateMachine.Enemy.Controller.Move(stateMachine.Enemy.ForceReceiver.Movement * Time.deltaTime);
    }

    // ํƒ€๊ฒŸ์„ ๋ฐฉํ–ฅ์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ฒŒ ํ•œ๋‹ค.
    private Vector3 GetMovementDirection()
    {
        return (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).normalized;
    }

    private void Move(Vector3 direction)
    {
        float movementSpeed = GetMovementSpeed();
        stateMachine.Enemy.Controller.Move(((direction * movementSpeed) + stateMachine.Enemy.ForceReceiver.Movement) * Time.deltaTime);
    }

    private void Rotate(Vector3 direction)
    {
        if (direction != Vector3.zero)
        {
            direction.y = 0;
            Quaternion targetRotation = Quaternion.LookRotation(direction);

            stateMachine.Enemy.transform.rotation = Quaternion.Slerp(stateMachine.Enemy.transform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
        }
    }

    protected float GetMovementSpeed()
    {
        float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;

        return movementSpeed;
    }

    protected float GetNormalizedTime(Animator animator, string tag)
    {
        AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
        AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);

        if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
        {
            return nextInfo.normalizedTime;
        }
        else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag))
        {
            return currentInfo.normalizedTime;
        }
        else
        {
            return 0f;
        }
    }

    // Enemy์—์„œ์˜ ์ถ”๊ฐ€ ์ฝ”๋“œ
    protected bool IsInChaseRange()
    {
        // if (stateMachine.Target.IsDead) { return false; }

        // Enemy์™€ Target์˜ ๊ฑฐ๋ฆฌ๋ฅผ ์žฌ๊ณ  ๊ทธ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ•œ๋‹ค.
        // sqrMagnitude : (ํ”ผํƒ€๊ณ ๋ผ์Šค)์ œ๊ณฑ๊ทผ์„ ํ’€์ง€ ์•Š์€ Magnitude, ์ œ๊ณฑ๊ทผ์„ ํ‘ธ๋Š” ์—ฐ์‚ฐ์ด ๋ฌด๊ฒ๋‹ค ๋ณด๋‹ˆ
        float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
        // ์—ฌ๊ธฐ์—์„œ ๊ณฑํ•˜๊ธฐ๋ฅผ ํ•œ๋ฒˆ ๋” ํ•ด์ค€๋‹ค.
        return playerDistanceSqr <= stateMachine.Enemy.Data.PlayerChasingRange * stateMachine.Enemy.Data.PlayerChasingRange;
    }
}

EnemyIdleState.cs ๋งŒ๋“ค๊ธฐ

public class EnemyIdleState : EnemyBaseState
{
    public EnemyIdleState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 0f;

        base.Enter();
        // ํ”Œ๋ ˆ์ด์–ด์™€ ๊ณต์œ ํ•ด์„œ ์“ธ ๊ฒƒ์ด๋‹ค ๋ณด๋‹ˆ ํ•œ๋ฒˆ์— ๋‘๊ฐœ๋ฅผ ์žก์•„์ค€๋‹ค.
        StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
        StartAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
        StopAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
    }

    public override void Update()
    {
        if (IsInChaseRange())   // ๊ทผ๊ฑฐ๋ฆฌ๊นŒ์ง€ ๋„๋‹ฌ์„ ํ–ˆ๋‹ค๋ฉด
        {
            stateMachine.ChangeState(stateMachine.ChasingState);
            return;
        }
    }
}

EnemyChasingState.cs ๋งŒ๋“ค๊ธฐ

public class EnemyChasingState : EnemyBaseState
{
    public EnemyChasingState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 1;
        base.Enter();
        StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
        StartAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
        StopAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (!IsInChaseRange()) // ์ซ“์•„๊ฐ€๋Š” ๊ฑฐ๋ฆฌ ๋‚ด์— ์—†๋‹ค๋ฉด
        {
            stateMachine.ChangeState(stateMachine.IdlingState); // Idle๋กœ ์ „ํ™˜
            return;
        }
        else if (IsInAttackRange()) // ๊ณต๊ฒฉ ๊ฑฐ๋ฆฌ ์•ˆ์— ์žˆ๋‹ค๋ฉด
        {
            stateMachine.ChangeState(stateMachine.AttackState); // Attack๋กœ ์ „ํ™˜
            return;
        }
    }

    private bool IsInAttackRange()
    {
        // if (stateMachine.Target.IsDead) { return false; }

        // ํƒ€๊ฒŸ๊ณผ์˜ ๊ฑฐ๋ฆฌ ๋น„๊ตํ•ด์„œ
        float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
        // AttackRange๋ž‘ ๋น„๊ต
        return playerDistanceSqr <= stateMachine.Enemy.Data.AttackRange * stateMachine.Enemy.Data.AttackRange;
    }
}



Animator Controller ์ˆ˜์ •

  • BaseAttack(Bool) Parameter ์ถ”๊ฐ€

  • Transition ์ˆ˜์ •

PlayerAnimationData.cs ์ˆ˜์ •

'''์ƒ๋žต
[SerializeField] private string baseAttackParameterName = "BaseAttack";
'''์ƒ๋žต
public int BaseAttackParameterHash { get; private set; }

EnemyAttackState.cs ๋งŒ๋“ค๊ธฐ

public class EnemyAttackState : EnemyBaseState
{
    private bool alreadyAppliedForce;

    public EnemyAttackState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 0;
        base.Enter();
        StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
        StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
        StopAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);

    }

    public override void Update()
    {
        base.Update();

        ForceMove();

        float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
        if (normalizedTime < 1f)
        {
            if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
                TryApplyForce();

        }
        else
        {
            if (IsInChaseRange())   // ๊ฑฐ๋ฆฌ ์ฒดํฌ
            {
                stateMachine.ChangeState(stateMachine.ChasingState);
                return;
            }
            else
            {
                stateMachine.ChangeState(stateMachine.IdlingState);
                return;
            }
        }

    }

    private void TryApplyForce()
    {
        if (alreadyAppliedForce) return;
        alreadyAppliedForce = true;

        stateMachine.Enemy.ForceReceiver.Reset();

        stateMachine.Enemy.ForceReceiver.AddForce(stateMachine.Enemy.transform.forward * stateMachine.Enemy.Data.Force);

    }
}





โž” ์˜ค๋ธŒ์ ํŠธ ์ˆ˜์ •ํ•˜๊ธฐ

์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ

  • ํ”Œ๋ ˆ์ด์–ด ๋ณต์ œ - Enemy๋กœ ์ˆ˜์ •

  • Player, Player Input โž” Remove Component

  • Enemy ์ถ”๊ฐ€

  • EnemySO ์ƒ์„ฑ ํ›„ ์—ฐ๊ฒฐ

ํ”Œ๋ ˆ์ด์–ด ํƒœ๊ทธ ์„ค์ •

  • ํ”Œ๋ ˆ์ด์–ด ์˜ค๋ธŒ์ ํŠธ โž” Tag "Player" ์ˆ˜์ •











๐Ÿ“Œ ํ”ผ๊ฒฉ ๋งŒ๋“ค๊ธฐ

โž” ํ”ผ๊ฒฉ ์‹œ์Šคํ…œ ๊ตฌ์„ฑํ•˜๊ธฐ

CharacterHealth.cs ๋งŒ๋“ค๊ธฐ

public class Health : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;
    private int health;
    public event Action OnDie;

    public bool IsDead => health == 0;

    private void Start()
    {
        health = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        if (health == 0) return;
        health = Math.Max(health - damage, 0);  // 0๋ณด๋‹ค ๋ฐ‘์œผ๋กœ ๋‚ด๋ ค๊ฐ€๋Š”๊ฑฐ ๋ฐฉ์ง€

        if (health == 0)
            OnDie?.Invoke();

        Debug.Log(health);
    }
}

Weapon.cs ๋งŒ๋“ค๊ธฐ

public class Weapon : MonoBehaviour
{
    [SerializeField] private Collider myCollider;

    private int damage;
    private float knockback;

    // <Collider> : ๋ชจ๋“  Collider๋“ค์˜ ์ƒ์œ„ Class
    private List<Collider> alreadyColliderWith = new List<Collider>();

    private void OnEnable()
    {
        alreadyColliderWith.Clear();    // ์ดˆ๊ธฐํ™”
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other == myCollider) return; // ๋‚˜ ์ž์‹ ?
        if (alreadyColliderWith.Contains(other)) return; // ์ด๋ฏธ ํฌํ•จ์ด ๋˜์–ด์žˆ๋‚˜?

        alreadyColliderWith.Add(other); // ๋‘˜ ๋‹ค ์•„๋‹ˆ๋ผ๋ฉด ์ถ”๊ฐ€(.Add)

        if(other.TryGetComponent(out Health health))
        {
            health.TakeDamage(damage);
        }

        if(other.TryGetComponent(out ForceReciver forceReceiver))
        {
            Vector3 direction = (other.transform.position - myCollider.transform.position).normalized;
            forceReceiver.AddForce(direction * knockback);
        }

    }

    public void SetAttack(int damage, float knockback)
    {
        this.damage = damage;
        this.knockback = knockback;
    }
}

EnemyAttackState.cs ์ˆ˜์ •

'''์ƒ๋žต
private bool alreadyAppliedDealing;

'''์ƒ๋žต
public override void Enter()
{
    alreadyAppliedForce = false;
    alreadyAppliedDealing = false;
    stateMachine.MovementSpeedModifier = 0;
    base.Enter();
    StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
    StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);


'''์ƒ๋žต
public override void Update()
{
    base.Update();

    ForceMove();

    float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
    if (normalizedTime < 1f)
    {
        if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
            TryApplyForce();

        // ๊ณต๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ง„ํ–‰ ์ค‘์— ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง„ํ–‰์†๋„์— if๋ฌธ์„ ๊ฑธ์–ด weapon์˜ SetActive๋ฅผ
        // ์ผœ์ฃผ์–ด ์ดˆ๊ธฐํ™”(OnEnable)ํ›„ ๋ฐ๋ฏธ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ๋ฐ›๊ฒŒ ํ•˜๊ณ (Weapon.cs) ๊ณต๊ฒฉ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋, ๊ณต๊ฒฉ์ด ๋๋‚˜๋ฉด
        // weapon์˜ SetActive๋ฅผ ๊บผ์ฃผ์–ด ๋ฐ๋ฏธ์ง€๋ฅผ ๋ฐ›์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค.
        // ๋”œ์„ ์•„์ง ์•ˆ๋„ฃ์€ ์ƒํƒœ && normalizedTime ์ด StartTime๋ณด๋‹ค ์ปค์กŒ๋‹ค๋ฉด?
        if (!alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_Start_TransitionTime)
        {
            stateMachine.Enemy.Weapon.SetAttack(stateMachine.Enemy.Data.Damage, stateMachine.Enemy.Data.Force);
            stateMachine.Enemy.Weapon.gameObject.SetActive(true);
            alreadyAppliedDealing = true;
        }

        // ๋”œ์„ ๋„ฃ์€ ์ƒํƒœ && EndTime์„ ๋„˜์—ˆ๋‹ค๋ฉด?
        if (alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_End_TransitionTime)
        {
            stateMachine.Enemy.Weapon.gameObject.SetActive(false);
        }
    }
    else
    {
        if (IsInChaseRange())   // ๊ฑฐ๋ฆฌ ์ฒดํฌ
        {
            stateMachine.ChangeState(stateMachine.ChasingState);
            return;
        }
        else
        {
            stateMachine.ChangeState(stateMachine.IdlingState);
            return;
        }
    }

}

Enemy.cs ์ˆ˜์ •

'''์ƒ๋žต
[field: SerializeField] public Weapon Weapon { get; private set; }
public CharacterHealth CharacterHealth { get; private set; }

void Awake()
{
'''์ƒ๋žต
    CharacterHealth = GetComponent<CharacterHealth>();

    stateMachine = new EnemyStateMachine(this);
}

private void Start()
{
    stateMachine.ChangeState(stateMachine.IdlingState);
    CharacterHealth.OnDie += OnDie;
}

'''์ƒ๋žต

void OnDie()
{
    Animator.SetTrigger("Die");
    enabled = false;    // ์˜ค๋ธŒ์ ํŠธ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”.
}

}





โž” ์ ์šฉํ•˜๊ธฐ

Weapon ์˜ค๋ธŒ์ ํŠธ ์ค€๋น„ํ•˜๊ธฐ

  • Weapon ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ

  • ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

  • Weapon ์ถ”๊ฐ€ - Collider ์„ค์ •

Enemy ์˜ค๋ธŒ์ ํŠธ ์„ค์ •

  • Enemy ์ปดํฌ๋„ŒํŠธ์— Weapon ์„ค์ •

Animator Controller ์ˆ˜์ •

  • Die๋ฅผ ํŠธ๋ฆฌ๊ฑฐ๋กœ ๋งŒ๋“ค๊ธฐ
    ์ฃฝ๋Š” ๋ชจ์…˜ ์—ฐ๊ฒฐ

[Player๋„ ๋™์ผํ•˜๊ฒŒ ์„ค์ • (Player.cs, Weapon ์˜ค๋ธŒ์ ํŠธ]












0๊ฐœ์˜ ๋Œ“๊ธ€