paint-brush
Unity에서 2D 캐릭터 컨트롤러를 만드는 방법: 1부~에 의해@deniskondratev
547 판독값
547 판독값

Unity에서 2D 캐릭터 컨트롤러를 만드는 방법: 1부

~에 의해 Denis Kondratev15m2024/04/22
Read on Terminal Reader

너무 오래; 읽다

이 문서에서는 캐릭터 이동을 단순화하는 'Rigidbody2D'에 대한 새로운 'Slide' 방법을 통합하는 데 중점을 두고 Unity에서 2D 캐릭터 컨트롤러를 만드는 과정을 자세히 살펴봅니다. 설정, 물리적 동작, 충돌 처리 및 이동 제약 조건을 다루며 다양한 2D 플랫폼 게임에 적용하고 확장할 수 있는 캐릭터 컨트롤러를 개발하기 위한 기본 가이드를 제공합니다.
featured image - Unity에서 2D 캐릭터 컨트롤러를 만드는 방법: 1부
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item


2D 플랫폼 게임의 핵심 요소 중 하나는 주인공입니다. 그것이 움직이고 제어되는 방식은 아늑한 옛날 게임이든 역동적인 슬래셔이든 상관없이 게임의 분위기를 크게 형성합니다. 따라서 캐릭터 컨트롤러를 만드는 것은 플랫폼 게임 개발에 있어 중요한 초기 단계입니다.


이 기사에서는 캐릭터를 처음부터 생성하는 과정을 철저하게 검토하고 물리 법칙을 준수하면서 레벨을 이동하도록 가르칠 것입니다. 이미 캐릭터 컨트롤러를 만든 경험이 있더라도 Unity 2023의 혁신에 대해 배우고 싶을 것입니다. 놀랍게도 오랫동안 기다려온 Slide 메서드가 Rigidbody2D 구성 요소에 추가되어 다음과 같이 캐릭터 컨트롤러 작성을 크게 단순화했습니다. Kinematic 모드에서 Rigidbody2D 보다 효과적으로 사용할 수 있습니다. 이전에는 이 모든 기능을 수동으로 구현해야 했습니다.


기사를 읽는 것뿐만 아니라 실제로 사용해 보고 싶다면 GitHub 저장소 Treasure Hunters 에서 레벨 템플릿을 다운로드하는 것이 좋습니다. 여기에는 캐릭터 테스트에 필요한 자산과 준비된 레벨이 이미 포함되어 있습니다.


캐릭터의 기초 마련하기

플랫폼 게임의 경우 게임에는 수직 및 수평 표면만 있고 중력은 엄격하게 아래쪽으로 향한다는 규칙을 설정했습니다. 이는 특히 벡터 수학을 탐구하고 싶지 않은 경우 초기 단계에서 플랫포머 생성을 크게 단순화합니다.


앞으로는 Unity에서 2D 플랫폼 게임의 메커니즘 생성을 탐구하는 Treasure Hunters 프로젝트에서 이러한 규칙에서 벗어날 수도 있습니다. 그러나 그것은 다른 기사의 주제가 될 것입니다.


움직임이 수평 표면을 따라 수행된다는 결정에 따라 캐릭터의 기본은 직사각형이 됩니다. 기울어진 표면을 사용하려면 캡슐 모양의 충돌기 및 슬라이딩과 같은 추가 메커니즘을 개발해야 합니다.


먼저 장면에 빈 객체를 만들고 이름을 Captain으로 지정합니다. 이것이 주인공이 됩니다. Rigidbody2DBoxCollider2D 구성 요소를 개체에 추가합니다. Unity에 내장된 물리 기능을 계속 활용하면서 캐릭터의 움직임을 제어할 수 있도록 Rigidbody2D 유형을 Kinematic으로 설정합니다. 또한 Freeze Rotation Z 옵션을 활성화하여 Z축을 따라 캐릭터의 회전을 잠급니다.


설정은 아래 그림과 같아야 합니다.


이제 선장에게 외모를 추가해 보겠습니다. Assets/Textures/Treasure Hunters/Captain Clown Nose/Sprites/Captain Clown Nose/Captain Clown Nose with Sword/09-Idle Sword/Idle Sword 01.png에서 텍스처를 찾아 단위당 픽셀 값을 32로 설정합니다. 우리는 종종 텍스처가 이 해상도로 생성되므로 이 과정에서는 이 값을 사용합니다. 스프라이트 모드를 단일로 설정하고 편의상 피벗을 하단으로 설정합니다. 적용 버튼을 클릭하여 변경 사항을 적용하는 것을 잊지 마십시오. 모든 설정이 올바르게 완료되었는지 확인하십시오.



이 문서에서는 캐릭터 애니메이션을 다루지 않으므로 지금은 하나의 스프라이트만 사용하겠습니다. Captain 개체에서 Appearance라는 중첩 개체를 만들고 여기에 Sprite Renderer 구성 요소를 추가하여 이전에 구성한 스프라이트를 지정합니다.



선장의 이미지를 확대했을 때 잘못된 스프라이트 설정으로 인해 이미지가 상당히 흐릿하다는 것을 알았습니다. 이 문제를 해결하려면 프로젝트 창에서 Idle Sword 01 텍스처를 선택하고 필터 모드를 포인트(필터 없음)로 설정하세요. 이제 이미지가 훨씬 좋아 보입니다.


캐릭터 충돌기 설정

현재 충돌체의 위치와 캡틴의 이미지가 일치하지 않으며 충돌체가 우리의 요구에 비해 너무 큰 것으로 나타났습니다.

이 문제를 수정하고 캐릭터의 피벗을 아래쪽에 올바르게 배치해 보겠습니다. 우리는 이 규칙을 게임의 모든 개체에 적용하여 크기에 관계없이 동일한 수준에 배치할 수 있도록 할 것입니다.


이 프로세스를 쉽게 관리하려면 아래와 같이 피벗 세트가 있는 토글 도구 핸들 위치를 사용하십시오.


다음 단계는 영웅의 충돌체를 조정하여 피벗 포인트가 정확히 중앙 하단에 오도록 하는 것입니다. 충돌체의 크기는 캐릭터의 크기와 정확히 일치해야 합니다. 충돌체의 오프셋 및 크기 매개변수와 중첩된 Appearance 개체의 위치를 조정하여 필요한 정밀도를 얻습니다.


충돌체의 Offset.X 매개변수에 특별한 주의를 기울이십시오. 해당 값은 엄격하게 0이어야 합니다. 이렇게 하면 개체 중심을 기준으로 충돌체의 대칭 배치가 보장됩니다. 이는 후속 캐릭터 왼쪽 및 오른쪽 회전에 매우 중요합니다. Transform.Scale.X의 값은 -1과 1로 변경됩니다. 충돌체는 그대로 유지되어야 하며 시각적 회전은 자연스럽게 보여야 합니다.


캐릭터의 물리적 행동 소개

무엇보다도 주요 영웅, NPC, 적 등의 캐릭터가 실제 세계와 상호 작용하도록 가르치는 것이 중요합니다. 예를 들어, 캐릭터는 평평한 표면 위를 걷고, 뛰어내릴 수 있어야 하며, 중력의 영향을 받아 다시 땅으로 끌어당길 수 있어야 합니다.


Unity에는 몸체의 움직임을 제어하고, 충돌을 처리하고, 객체에 외부 힘의 효과를 추가하는 물리 엔진이 내장되어 있습니다. 이는 모두 Rigidbody2D 구성 요소를 사용하여 구현됩니다. 그러나 캐릭터의 경우 개발자가 개체의 동작을 더 잘 제어할 수 있도록 하면서 실제 세계와 상호 작용하는 보다 유연한 도구를 갖는 것이 유용합니다.


앞서 언급했듯이 이전 버전의 Unity에서는 개발자가 이 모든 로직을 직접 구현해야 했습니다. 그러나 Unity 2023에서는 새로운 메서드인 Slide Rigidbody2D 구성 요소에 추가되어 물리적 개체를 유연하게 제어하는 동시에 수행되는 움직임에 대해 필요한 모든 정보를 제공할 수 있습니다.


물리적 세계에서 캐릭터를 이동하기 위한 기본 논리를 포함하는 CharacterBody 클래스를 만드는 것부터 시작해 보겠습니다. 이 클래스는 이미 캐릭터에 추가한 Kinematic 모드의 Rigidbody2D 사용합니다. 따라서 가장 먼저 할 일은 이 구성 요소에 대한 참조를 추가하는 것입니다.


 public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; }


때로는 캐릭터 움직임의 역동성을 위해 중력이 평소보다 더 강하게 작용할 필요가 있습니다. 이를 달성하기 위해 초기 값이 1인 중력 영향 요소를 추가할 것이며 이 요소는 0보다 작을 수 없습니다.


 [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f;


또한 통과할 수 없는 표면으로 간주할 객체를 정의해야 합니다. 이를 위해 필요한 레이어를 지정할 수 있는 필드를 생성하겠습니다.


 [SerializeField] private LayerMask _solidLayers;


예를 들어 중력을 포함한 일부 외부 영향으로 인해 캐릭터가 지나치게 빠른 속도로 발전하는 것을 방지하기 위해 캐릭터의 속도를 제한합니다. 초기값을 30으로 설정하고 인스펙터에서 0보다 작은 값을 설정하는 기능을 제한합니다.


 [Min(0)] [SerializeField] private float _maxSpeed = 30;


표면을 따라 이동할 때 캐릭터 사이의 거리가 충분히 작다면 캐릭터가 항상 표면에 달라붙기를 원합니다.


 [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f;


게임의 표면은 수평 또는 수직만 사용하기로 결정했지만 만일의 경우를 대비하여 캐릭터가 안정적으로 설 수 있는 표면의 최대 경사 각도를 초기 값 45°로 지정하겠습니다.


 [Range(0, 90)] [SerializeField] private float _maxSlop = 45f;


인스펙터를 통해 캐릭터의 현재 속도와 상태도 확인하고 싶기 때문에 SerializeField 속성이 있는 두 개의 필드를 추가하겠습니다.


 [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; }


예, 여기서는 아직 정의되지 않은 새 엔터티인 CharacterState 소개했습니다. 이에 대해 더 자세히 논의하겠습니다.


문자 상태

플랫폼 개발을 단순화하기 위해 두 가지 주요 캐릭터 상태만 정의하겠습니다.

첫 번째는 Grounded 로, 캐릭터가 표면에 단단히 서 있는 상태입니다. 이 상태에서 캐릭터는 표면을 따라 자유롭게 움직이고 뛰어 내릴 수 있습니다.


두 번째는 캐릭터가 공중에 떠 있는 자유낙하 상태인 에어본(Airborne) 이다. 이 상태의 캐릭터 행동은 플랫폼 게임의 특성에 따라 달라질 수 있습니다. 현실에 가까운 일반적인 경우, 캐릭터는 초기 추진력의 영향을 받아 움직이며 행동에 영향을 미칠 수 없습니다. 그러나 플랫폼 게임에서는 편의성과 게임플레이 역학을 위해 물리학이 단순화되는 경우가 많습니다. 예를 들어 많은 게임에서 자유 낙하하는 경우에도 캐릭터의 수평 이동을 제어할 수 있습니다. 우리의 경우에는 이중 점프의 인기 있는 메커니즘뿐만 아니라 공중에서 추가 점프를 허용하는 것도 가능합니다.


코드에서 캐릭터의 상태를 표현해 보겠습니다.


 /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne }


더 많은 상태가 있을 수 있다는 점은 주목할 가치가 있습니다. 예를 들어, 게임에 경사면이 포함된 경우 캐릭터는 오른쪽이나 왼쪽으로 자유롭게 이동할 수는 없지만 경사면을 따라 미끄러질 수 있는 슬라이딩 상태에 있을 수 있습니다. 이러한 상태에서 캐릭터는 점프하여 경사면에서 밀어낼 수도 있지만 경사면 방향으로만 가능합니다. 또 다른 가능한 경우는 수직 벽을 따라 미끄러지는 경우인데, 중력의 영향이 약해져서 캐릭터가 수평으로 밀려날 수 있습니다.


이동 속도 제한

우리는 이미 비공개 필드 _velocity 정의했지만 캐릭터의 최대 속도를 제한하면서 외부에서 이 값을 가져오고 설정할 수 있어야 합니다. 이를 위해서는 주어진 속도 벡터를 최대 허용 속도와 비교해야 합니다.


이는 속도 벡터의 길이를 계산하거나 수학 용어로 그 크기를 계산하여 수행할 수 있습니다. Vector2 구조에는 이 작업을 수행할 수 있는 magnitude 속성이 이미 포함되어 있습니다. 따라서 전달된 벡터의 크기가 최대 허용 속도를 초과하는 경우 벡터의 방향은 유지하되 크기는 제한해야 합니다. 이를 위해 _maxSpeed 에 정규화된 속도 벡터를 곱합니다(정규화된 벡터는 방향은 동일하지만 크기는 1인 벡터입니다).


코드에서는 다음과 같습니다.


 public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; }


이제 벡터의 크기가 어떻게 계산되는지 자세히 살펴보겠습니다. 이는 다음 공식으로 정의됩니다.



제곱근을 계산하는 것은 리소스 집약적인 작업입니다. 대부분의 경우 속도는 최대값을 초과하지 않지만 적어도 사이클당 한 번은 이 비교를 수행해야 합니다. 그러나 벡터 크기의 제곱과 최대 속도의 제곱을 비교하면 이 작업을 상당히 단순화할 수 있습니다.


이를 위해 최대 속도의 제곱을 저장하는 추가 필드를 도입하고 Awake 메서드에서 한 번 계산합니다.


 private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; }


이제 속도 설정을 더욱 최적으로 수행할 수 있습니다.


 public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; }


따라서 불필요한 계산을 피하고 캐릭터의 이동 속도 처리 성능을 향상시킵니다.


강체 이동 방법

앞서 언급했듯이 Unity는 CharacterBody 개발을 크게 단순화하는 새로운 Slide() 메서드를 추가했습니다. 그러나 이 방법을 사용하기 전에 물체가 공간에서 움직일 규칙을 정의하는 것이 필요합니다. 이 동작은 Rigidbody2D.SlideMovement 구조에 의해 설정됩니다.


새로운 필드 _slideMovement 도입하고 해당 값을 설정해 보겠습니다.


 private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; }



maxIterations 충돌의 결과로 객체가 방향을 변경할 수 있는 횟수를 결정한다는 점을 설명하는 것이 중요합니다. 예를 들어 캐릭터가 벽 옆 공중에 있고 중력이 작용하는 동안 플레이어가 캐릭터를 오른쪽으로 이동하려고 하는 경우입니다. 따라서 Slide() 메서드를 호출할 때마다 오른쪽과 아래쪽으로 향하는 속도 벡터가 설정됩니다. 벽에 부딪히면 모션 벡터가 다시 계산되고 개체는 계속 아래쪽으로 이동합니다.


이러한 상황에서 maxIterations 값을 1로 설정하면 객체가 벽에 부딪혀 멈춰 사실상 거기에 갇히게 됩니다.


maxIterationslayerMask 의 값은 이전에 정의되었습니다. 다른 필드에 대한 자세한 내용은 공식 구조 문서를 참조하세요.


마지막으로 캐릭터 이동

이제 Captain이 움직일 수 있도록 모든 것이 설정되었습니다. 우리는 물리 처리를 위해 설계된 Unity의 콜백인 FixedUpdate 에서 이 작업을 수행할 것입니다. 지난 몇 년 동안 Unity 팀은 2D 물리 처리 방식을 크게 개선했습니다. 현재 처리는 Update 콜백에서 수행되거나 필요한 메소드를 직접 호출하여 수행될 수도 있습니다.


그러나 이 예에서는 전통적이고 입증된 FixedUpdate 방법을 사용합니다. 계속하기 전에 Time.fixedDeltaTime 값에 대해 몇 마디 언급하는 것이 좋습니다.


게임 물리학의 예측 가능성을 보장하기 위해 시뮬레이션은 고정된 시간 간격으로 반복적으로 수행됩니다. 이는 FPS 또는 지연의 변화가 개체 동작에 영향을 미치지 않음을 보장합니다.


각 주기가 시작될 때 물체에 대한 중력의 영향을 설명합니다. 중력은 자유 낙하 가속도의 벡터로 주어지기 때문에 시간 Δt 에 따른 물체의 속도 Δv 변화를 다음 공식으로 계산할 수 있습니다.




여기서 a 물체의 일정한 가속도입니다. 우리의 경우, 우리가 도입한 계수인 Physics2D.gravity * GravityFactor 고려하면 중력으로 인한 가속도입니다. 따라서 Δv 다음과 같이 계산할 수 있습니다.


 Time.fixedDeltaTime * GravityFactor * Physics2D.gravity


속도를 변경하는 최종 결과는 다음과 같습니다.


 Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity;


이제 캐릭터의 강체 이동을 수행할 수 있습니다.


 var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement);


slideResults 변수는 SlideResults 구조의 값이며 이동 결과를 저장합니다. 이 결과의 주요 필드는 이동 중 표면과의 충돌 결과인 slideHit 과 캐릭터가 안정된 표면에 서 있는지 확인하는 데 도움이 되는 하향 캐스트의 결과인 surfaceHit 입니다.


충돌 처리

표면과 충돌할 때 해당 표면을 향한 캐릭터의 속도를 제한하는 것이 중요합니다. 간단한 예는 캐릭터가 땅에 가만히 서 있는 경우 중력의 영향으로 계속 속도를 얻으면 안 된다는 것입니다. 각 사이클이 끝나면 속도는 0이 되어야 합니다. 마찬가지로 위쪽으로 이동하여 천장에 닿으면 캐릭터가 수직 속도를 모두 잃고 아래쪽으로 이동하기 시작해야 합니다.


충돌 결과인 slideHitsurfaceHit 충돌 표면의 법선을 포함하는 RaycastHit2D 구조의 값으로 표시됩니다.


속도 제한은 속도 벡터 자체에서 충돌 법선에 대한 원래 속도 벡터의 투영을 빼서 계산할 수 있습니다. 이것은 내적(dot product)을 사용하여 수행됩니다. 이 작업을 수행할 메서드를 작성해 보겠습니다.


 private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; }


이제 이 메서드를 FixedUpdate 에 통합해 보겠습니다. 여기서, surfaceHit 의 경우 물체가 표면에 있는지 확인하는 캐스트는 항상 지면과의 접촉을 확인하기 위해 수행되므로 아래로 향하는 경우에만 속도를 제한합니다.


 private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } }


이 구현을 통해 캐릭터의 움직임을 올바르게 관리하고, 다양한 표면과 충돌할 때 원치 않는 가속을 피하고, 게임 내 캐릭터의 움직임을 예측 가능하고 부드럽게 유지할 수 있습니다.


캐릭터의 상태 확인

각 주기가 끝나면 캐릭터가 단단한 표면(바닥 상태)에 있는지 아니면 자유 낙하(또는 우리가 정의한 대로 제어된 낙하, 즉 공중 상태)에 있는지 확인해야 합니다.


캐릭터가 Grounded 상태에 있는 것으로 간주하려면 주로 수직 속도가 0 또는 음수여야 하며, 이는 _velocity.y 값으로 결정됩니다.


또 다른 중요한 기준은 캐릭터의 발 아래에 표면이 존재하는지 여부입니다. 이는 Rigidbody 이동 결과, 즉 surfaceHit 의 존재를 통해 식별됩니다.


세 번째 요소는 표면의 경사 각도로, 이 표면의 법선, 즉 surfaceHit.normal 값을 기준으로 분석합니다. 이 각도를 _maxSlop (캐릭터가 안정적으로 서 있을 수 있는 표면의 최대 각도)와 비교해야 합니다.


완전히 수직인 표면의 경우 법선은 완전히 수평입니다. 즉, 벡터 값은 (1, 0) 또는 (-1, 0)입니다. 수평 표면의 경우 법선 값은 (0, 1)입니다. 경사각이 작을수록 y 값이 커집니다. 각도 alpha 의 경우 이 값은 다음과 같이 계산할 수 있습니다.



각도는 도 단위로 주어지고 $\cos$ 함수에는 라디안이 필요하므로 공식은 다음과 같이 변환됩니다.



이를 위해 새 필드를 도입하고 Awake 메서드에서 계산해 보겠습니다.


 private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... }


이제 위의 모든 조건을 확인하면서 FixedUpdate 에서 코드를 업데이트해 보겠습니다.


 if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; }


이 논리를 사용하면 캐릭터가 땅에 있는 시기를 정확하게 판단하고 상태 변화에 올바르게 반응할 수 있습니다.


Captain에 CharacterBody 추가하기

이제 CharacterBody 구성 요소가 준비되었으므로 마지막 단계는 이를 Captain에 추가하는 것입니다. 장면에서 Captain 개체를 선택하고 여기에 CharacterBody 구성 요소를 추가합니다.

위 그림과 같이 Rigidbody를 구성하는 것을 잊지 마십시오. Gravity Factor를 3으로 설정하고 Solid Layer의 Default 옵션을 선택합니다.


이제 게임을 시작하고 Velocity에 대한 다양한 값을 설정하여 캐릭터가 장면에서 어떻게 움직이는지 관찰할 수 있습니다.

지금은 마무리

물론, 여전히 캐릭터 컨트롤을 추가해야 합니다. 하지만 이 글은 이미 꽤 길어졌기 때문에 다음 글 "Unity에서 캐릭터 컨트롤러 2D 만들기: 파트 2"에서 새로운 입력 시스템을 사용한 캐릭터 제어에 대해 자세히 설명하겠습니다.


이 기사에 설명된 전체 프로젝트를 Treasure Hunters 에서 다운로드하고 어려움이 발생할 경우 실제로 모든 것을 확인할 수 있습니다. 캐릭터 컨트롤러를 개발하는 것은 게임의 향후 개발을 결정하므로 2D 플랫폼 게임을 만드는 데 있어 핵심적인 측면입니다. 이는 주요 영웅이나 적의 행동에 새로운 기능이 얼마나 쉽게 추가되는지에 영향을 미칩니다. 그러므로, 자신만의 게임을 독립적으로 개발하기 위해서는 기본을 이해하는 것이 매우 중요합니다.