Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

동구의_C# & Unity_개발일지

2024.01.19 내일배움캠프 20일차 TIL_Unity (입문, 알고리즘) 본문

Unity

2024.01.19 내일배움캠프 20일차 TIL_Unity (입문, 알고리즘)

mongle_0l 2024. 1. 19. 09:14

오늘은 강의를 들으며 개인 과제도 같이 진행하였다.

강의에는 TopDownShooting이라는 게임을 만들고 개인과제는 스파르타 타운 만들기 (zep 클론)이다.

개인과제을 완성하는데 목표를 두고 있어 최대한 필수요구사항을 구현할 것이다!

 

필수요구사항

1. 캐릭터 만들기 (진행 중)

2. 캐릭터 이동 (진행 중)

3. 방 만들기 (미완료)

4. 카메라 따라가기 (미완료)

5. 캐릭터 애니메이션 추가 (미완료)

6. 이름 입력 시스템 (미완료)

7. 캐릭터 선택 시스템 (미완료)


알고리즘 코드카타 7일차

배열의 평균값

문제 설명
정수 배열 numbers가 매개변수로 주어집니다. numbers의 원소의 평균값을 return하도록 solution 함수를 완성해주세요.
using System;

public class Solution {
    public double solution(int[] numbers) {
        double answer = 0;
        int sum = 0;
        
        for(int i = 0; i < numbers.Length; i++)
        {
            sum = sum + numbers[i];
        }
        
        answer = (double)sum / (double)numbers.Length;
        return answer;
    }
}

 


2D 게임 기초 개발 시작

TopDown Shooting 프로젝트 셋팅
입력과 캐릭터 이동
충돌 처리와 맵 구성


01. 사용된 에셋

02. 프로젝트 준비하기

2-1) 프로젝트 생성

  • 유니티 허브에서 새로운 프로젝트를 생성
  • 프로젝트 이름을 설정하고, 저장할 위치를 선택
  • 2D 프로젝트로 설정하여 2D 게임을 개발할 수 있도록 준비

2-2) 레이아웃 설정

  • 에디터 창의 배치와 크기를 조정하여 작업 환경을 사용자에 맞게 구성
  • 드롭다운 메뉴를 통해 미리 정의된 레이아웃을 선택하거나, 창의 크기를 조정하고 분할하여 필요한 도구에 더 빠르게 접근
  • 레이아웃 설정을 통해 사용자는 개발 작업에 효율적으로 접근

03. 스크립트 작성 방법

3-1) 유니티 스크립트 작성

  • 프로젝트가 생성되면 Unity Editor를 열고, 스크립트를 작성할 수 있는 코드 편집기를 선택합니다. (예: Visual Studio)
  • 스크립트를 작성할 폴더를 생성하고, 해당 폴더에 새로운 C# 스크립트 파일을 만듭니다.
  • 스크립트 파일을 더블 클릭하여 코드 편집기에서 스크립트를 엽니다.
  • 스크립트 파일에는 게임의 로직, 오브젝트의 동작, 입력 처리 등을 정의하는 코드를 작성합니다.

3-2) 스크립트 작성 방법

  • MonoBehaviour를 상속받은 클래스를 작성합니다. 이 클래스는 유니티의 게임 오브젝트와 연결된 스크립트로 동작합니다.
  • 필요한 변수, 함수, 이벤트 등을 정의하고 구현합니다. 게임의 동작을 위한 로직을 작성합니다.
  • 필요한 Unity 함수들을 오버라이딩하여 원하는 동작을 구현할 수 있습니다. 예를 들어, Start(), Update(), FixedUpdate() 등을 활용할 수 있습니다.
  • 필요에 따라 다른 스크립트나 Unity의 컴포넌트와 상호작용하는 코드를 작성할 수 있습니다.

3-3) 스크립트 라이프 사이클

  • 게임 오브젝트의 생명 주기 동안 호출되는 특정한 메서드들의 순서와 타이밍
  • 게임 오브젝트의 생성, 초기화, 업데이트, 파괴 등과 관련된 작업을 수행
  1. Awake: 게임 오브젝트가 생성될 때 호출되는 메서드입니다. 주로 초기화 작업이 수행됩니다.
  2. Start: 게임 오브젝트가 활성화되어 게임 루프가 시작될 때 호출되는 메서드입니다. 초기 설정 및 시작 작업을 수행합니다.
  3. Update: 매 프레임마다 호출되는 메서드로, 게임 로직의 주요 업데이트가 이루어집니다.
  4. FixedUpdate: 물리 엔진 업데이트 시 호출되는 메서드입니다. 물리적인 시뮬레이션에 관련된 작업을 처리할 때 사용됩니다.
  5. LateUpdate: Update 메서드 호출 이후에 호출되는 메서드입니다. 다른 오브젝트의 업데이트가 완료된 후에 작업을 수행하는 데 유용합니다.
  6. OnEnable: 게임 오브젝트가 활성화될 때 호출되는 메서드입니다.
  7. OnDisable: 게임 오브젝트가 비활성화될 때 호출되는 메서드입니다.
  8. OnDestroy: 게임 오브젝트가 파괴될 때 호출되는 메서드입니다. 자원 정리 및 해제 작업이 수행됩니다.

04. 컴포넌트 이해

4-1) 유니티의 컴포넌트 방식

  • 게임 오브젝트에 부착되는 독립적인 기능 모듈
  • 각 컴포넌트는 특정한 작업을 수행하거나 기능을 제공하는 역할
  • 컴포넌트들을 게임 오브젝트에 추가하고 구성함으로써 게임의 동작과 특징을 결정

4-2) 기본 컴포넌트 예시

  1. Transform: 게임 오브젝트의 위치, 회전, 크기 등을 조정하는 데 사용됩니다.
  2. Rigidbody: 물리적인 효과를 게임 오브젝트에 적용할 수 있게 해줍니다.
  3. Collider: 충돌 감지를 처리하기 위해 사용되는 컴포넌트입니다.
  4. SpriteRenderer: 2D 그래픽을 표시하는 데 사용됩니다.
  5. AudioSource: 사운드를 재생하기 위해 사용되는 컴포넌트입니다.
  6. 사용자가 필요에 따라 컴포넌트를 직접 작성하고 추가할 수도 있습니다. 이를 통해 게임의 특정한 동작이나 기능을 개발자가 원하는 대로 커스터마이즈

05. 핵심 기능 소개

5-1) Pixels Per Unit (PPU)

  • Pixels Per Unit (PPU): 스프라이트의 픽셀 수와 해당 스프라이트가 게임 세계에서 차지하는 공간의 관계를 설명합니다. 예를 들어, PPU가 100이라면 스프라이트의 100픽셀은 게임 세계에서 1 유니티 단위를 나타냅니다.
  • 스프라이트의 크기: PPU 값이 클수록 스프라이트는 작아집니다. 이는 더 많은 픽셀이 동일한 게임 세계의 공간에 매핑되기 때문입니다.
  • 물리 시뮬레이션: PPU 값은 물리 시뮬레이션에 영향을 미칩니다. 높은 PPU 값은 더 작은 스프라이트를 생성하므로, 이는 더 높은 해상도의 물리 시뮬레이션을 가능하게 합니다.
  • 퍼포먼스: 높은 PPU 값은 더 많은 연산을 필요로 합니다. 이는 성능에 영향을 미칠 수 있습니다. 따라서, 필요한 만큼의 PPU 값을 설정하는 것이 중요합니다.
  • 일관성: 모든 스프라이트에 대해 일관된 PPU 값을 사용하는 것이 좋습니다. 이는 스프라이트간의 크기 비율을 일정하게 유지하고, 물리적 행동의 일관성을 보장하는 데 도움이 됩니다.

5-2) 계층 구조 ('Transform' 구조)

  • 각 게임 오브젝트는 Transform 컴포넌트를 가집니다.
  • Transform 컴포넌트는 게임 오브젝트의 위치, 회전, 및 크기(scale)를 정의합니다.
  • 게임 오브젝트는 다른 게임 오브젝트의 '자식'이 될 수 있습니다.
  • 부모 게임 오브젝트의 Transform이 변경되면(예: 이동, 회전, 크기 변경), 그 자식 오브젝트들의 Transform도 동일하게 적용됩니다.
  • 이렇게, 게임 오브젝트들 사이에 계층적인 관계가 형성되고 이를 '트리 구조'라고도 부릅니다.
  • 이 구조는 복잡한 씬을 구성하고 관리하는데 유용합니다. 예를 들어, 차량 게임 오브젝트 내부에 각각의 부품(바퀴, 핸들 등)을 자식 게임 오브젝트로 두면, 차량의 Transform이 변경될 때 각 부품들도 함께 움직입니다.

5-3) 로컬 좌표계(Local Coordinate System)와 월드 좌표계(World Coordinate System)

  1. 월드 좌표계(World Coordinate System): 월드 좌표계는 게임 세계의 전체적인 참조 프레임을 제공합니다. 이는 모든 게임 오브젝트가 공유하며, 월드 좌표계에서의 위치는 게임 환경 내에서 오브젝트의 절대적인 위치를 나타냅니다. 월드 좌표계는 변하지 않고 일정하게 유지됩니다.
  2. 로컬 좌표계(Local Coordinate System): 로컬 좌표계는 개별 게임 오브젝트에 대한 참조 프레임을 제공합니다. 오브젝트의 로컬 좌표계는 해당 오브젝트의 위치, 회전, 크기(scale)에 따라 변화합니다. 이 좌표계는 특히 부모-자식 관계에 있는 오브젝트들 사이에서 중요합니다. 자식 오브젝트의 로컬 좌표는 부모 오브젝트에 대한 상대적인 위치를 나타내며, 부모 오브젝트가 움직이면 그에 따라 자식 오브젝트의 월드 좌표도 변화하게 됩니다.

5-4) Input.GetAxis

  • Input.GetAxis는 유니티의 입력 시스템에서 사용되는 메서드입니다.
  • 이 메서드는 입력 축의 값을 반환합니다.
  • 입력 축은 주로 키보드나 조이스틱과 같은 입력 장치의 입력을 나타냅니다.
  • GetAxis 메서드는 -1부터 1 사이의 값을 반환하는데, 입력 장치의 움직임에 따라 해당 값이 변경됩니다.
  • 값이 0에 가까울수록 입력이 없거나 중립 상태를 나타내며, 양수 값은 양 방향 입력을, 음수 값은 음 방향 입력을 나타냅니다.
  • GetAxis 메서드는 주로 플레이어의 움직임, 회전, 점프 등을 처리하는 데 사용됩니다.
  • 예를 들어, 수평 이동을 처리하는 경우, 좌우 화살키의 입력에 따라 Input.GetAxis("Horizontal")을 사용하여 좌우 방향의 값을 얻을 수 있습니다.
  • 이 값은 플레이어의 이동 속도나 회전 속도와 같은 변수에 적용하여 게임 오브젝트를 제어할 수 있습니다.

5-5) TIme.deltaTime

  • 이전 프레임부터 현재 프레임까지의 경과 시간을 나타냅니다.
  • deltaTime은 게임의 프레임 속도에 상관없이 일정한 시간 간격으로 동작하는 게임을 만들 때 유용하게 사용됩니다.
  • 주로 움직임, 애니메이션, 물리 시뮬레이션 등에서 시간에 따른 변화를 조정하는 데 사용됩니다.
  • 예를 들어, transform.Translate(Vector3.forward * speed * Time.deltaTime)과 같이 사용하면 프레임 속도에 관계없이 speed만큼의 일정한 이동 속도를 보장할 수 있습니다.
  • Time.deltaTime은 초 단위의 값을 반환하며, 1초에 1에 가까운 값을 가집니다.
  • 게임의 로직이 매 프레임마다 일정한 속도로 실행되어야 할 때, deltaTime을 이용하여 이동, 회전, 애니메이션 등의 연산에 일정한 시간 간격을 적용할 수 있습니다.
  • 이를 통해 게임이 일정한 속도로 동작하고, 다양한 기기나 환경에서도 일관된 경험을 제공할 수 있습니다.

5-6) 접근 제한자와 직렬화 속성

  • public
    • 변수나 메서드가 외부에서 접근 가능하도록 공개
    • 다른 클래스나 스크립트에서 해당 멤버에 접근하여 값을 설정하거나 호출
  • private
    • 변수나 메서드가 같은 클래스 내에서만 접근 가능하도록 제한
    • 다른 클래스나 스크립트에서는 접근할 수 없고, 해당 클래스 내부에서만 사용
    • 보통 내부 상태를 관리하거나 내부 구현에 사용
  • SerializeField
    • private로 선언된 변수를 인스펙터에서 직접 접근
    • 기본적으로 private 변수는 인스펙터에 표시되지 않지만, SerializeField를 사용하면 해당 변수가 인스펙터에서 수정 가능한 필드로 표시
# 예시 코드
public class ExampleClass : MonoBehaviour
{
    public int publicVariable;
    private int privateVariable;
    
    [SerializeField]
    private int serializedFieldVariable;
    
    private void Start()
    {
        publicVariable = 10;  // 외부에서 접근 가능
        privateVariable = 20; // 클래스 내부에서만 접근 가능
        serializedFieldVariable = 30; // 인스펙터에서 접근 가능
    }
}

5-7) New Input System

New Input System의 핵심 개념들

  1. Input Action: 입력 행동을 정의합니다. 예를 들어 "점프", "공격" 등의 행동을 정의하고, 이러한 행동을 트리거하는 키 또는 버튼을 지정할 수 있습니다.
  2. Input Action Asset: 여러 개의 입력 행동을 그룹화하는 방법입니다. 이를 통해 재사용 가능한 입력 설정을 만들어 게임 내의 다른 캐릭터나 메뉴에 적용할 수 있습니다.
  3. Player Input Component: Unity의 New Input System에 추가된 새로운 컴포넌트로, 자동으로 입력 행동을 처리하고 해당 게임 오브젝트에 메시지를 보냅니다.

New Input System의 장점

  1. Cross-Platform Compatibility: New Input System은 다양한 플랫폼과 입력 장치에 대해 일관된 방식으로 작동합니다.
  2. Rebinding: 플레이어가 게임 내에서 자신의 입력 설정을 변경할 수 있도록 지원합니다.
  3. Multiplayer Support: 여러 플레이어가 동일한 장치에서 게임을 플레이하거나, 각각의 장치에서 게임을 플레이할 때 입력을 쉽게 처리할 수 있습니다.

06. 충돌과 각 컴포넌트의 이해

Unity에서 충돌과 관련하여 중요한 개념은 Collider와 Rigidbody 컴포넌트입니다.
  • 이 두 컴포넌트는 Unity에서 물리 시뮬레이션과 충돌 감지를 처리하는데 필수적입니다.
  1. Collider:
    1. Collider 컴포넌트는 게임 오브젝트에 물리적 형태를 부여합니다. 이는 충돌 감지를 가능하게 하며, 여러가지 종류의 Collider가 있습니다.
    2. 예를 들어 BoxCollider는 사각형, SphereCollider는 원형, MeshCollider는 복잡한 메시 형태의 Collider를 제공합니다. Collider는 자체적으로 물리적인 움직임을 제어할 수는 없지만 충돌을 감지할 수 있습니다.
  2. Rigidbody:
    1. Rigidbody 컴포넌트는 게임 오브젝트에 물리 법칙을 적용합니다. Rigidbody가 있는 오브젝트는 중력에 영향을 받고, 힘과 토크를 통해 움직일 수 있습니다.
    2. 또한 Rigidbody가 부착된 오브젝트는 다른 Rigidbody나 Collider와 충돌할 수 있습니다.

6-1) 충돌 발생 시

  • Unity는 OnCollisionEnter, OnCollisionStay, OnCollisionExit 등의 이벤트를 발생시킵니다.
  • 이 이벤트는 스크립트에서 처리하여 원하는 기능을 실행할 수 있습니다.
  • 예를 들어, 오브젝트가 땅에 닿으면 점프 가능 상태로 변경하거나, 오브젝트가 플레이어와 충돌하면 데미지를 입히는 등의 기능을 구현할 수 있습니다.

07. 타일맵

Unity의 Tilemap 시스템을 사용하면 이러한 타일 기반의 게임 환경을 쉽게 만들 수 있습니다. 이 시스템을 사용하면 작은 스프라이트를 통해 광대한 게임 환경을 구성할 수 있으며, 복잡한 게임 레벨을 쉽게 디자인하고, 편집하고, 조작할 수 있습니다.

7-1) Tilemap 시스템의 구성 요소

  1. Tilemap GameObject: Unity의 타일맵 구조를 구성하는 데 사용됩니다. Tilemap Grid의 자식으로 위치하고, 특정 타일의 배치를 관리합니다.
  2. Grid GameObject: 모든 타일맵이 위치하는 기본 격자를 나타냅니다.
  3. Tilemap Renderer: Tilemap의 모양을 실제로 그리는 역할을 합니다.
  4. Tilemap Collider 2D: 필요한 경우, Tilemap에 물리적인 경계를 추가하는 데 사용됩니다. 이를 통해 게임 캐릭터가 타일맵 환경과 상호작용할 수 있게 됩니다.
  5. Tile Assets: 개별 타일의 모양과 동작을 정의합니다. 여러 개의 타일을 묶어서 Tileset이라고 부르기도 합니다.

중요한 기능들을 정리해서 알아보자!

 

Player > 실제 부모 클래스

MainSprite > 자식 클래스

(계층 구조, Transform 구조)

Player는 월드좌표와 로컬 좌표가 동일(2, 0, 0) WHY? 부모가 없으니까!

MainSprtie는 실제 좌표는 (0, 0, 0)이나 월드 중심에서 2만큼 떨어져있다! (월드 좌표) 

여기서 MainSprite의 좌표를 (0, 1, 0)으로 이동시키면

부모는 2만큼 떨어져있고 자식은 1만큼 올라가있다.

세상에 중심으로 따지는 월드좌표는 (2, 1, 0)인데 로컬좌표는 부모을 기준으로 하여 (0, 1, 0) 이다.

 

Edit > Project Settings > Input Manager (간편히 입력을 받는 방법)

 

Input.GetAxis > 각각의 개별적인 키입력을 받자

void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");

        transform.position += new Vector3(x, y) * speed * Time.deltaTime;
    }
1. 여기에서 왜 Vector1도 아니고 vector2도 아니고 Vector3을 사용했을까?

(feat. Chat.GPT)
이 코드에서 Vector3을 사용하는 이유는 Unity의 3D 공간에서의 좌표를 다루기 때문입니다. Vector3는 Unity에서 3D 벡터를 나타내는 클래스이며, x, y, z 세 개의 성분을 가지고 있습니다.
일반적으로 2D 게임에서는 Vector2를 사용하여 좌표를 표현할 수 있습니다. 그러나 Unity에서는 내부적으로 3D 공간을 기반으로 하고 있어서, 2D 게임이라도 Vector3을 사용하여 좌표를 조작하는 것이 일반적입니다.

2. Time.deltaTime 이란?

(feat. Chat.GPT)
Time.deltaTime은 게임 프로그래밍에서 주로 사용되는 용어로, 이전 프레임과 현재 프레임 간의 시간 간격을 나타냅니다. 이 값은 게임이 실행되는 환경에 따라 다르며, 게임의 성능이나 실행 속도에 영향을 받지 않고 일정한 시간 간격을 제공하는 데 사용됩니다.
일반적으로 게임 루프는 프레임마다 일정한 간격으로 실행됩니다. 그러나 게임이 무거워지거나 가변적인 환경에서는 프레임 간격이 일정하지 않을 수 있습니다. Time.deltaTime은 이러한 시간 간격의 변동을 보상하여, 이전 프레임에서 현재 프레임으로 넘어갈 때 걸린 시간을 나타냅니다.
주로 이 값을 사용하여 움직임, 회전, 애니메이션 등을 계산할 때 사용합니다. 코드에서 보면, transform.position += new Vector3(x, y) * speed * Time.deltaTime;는 입력값에 따라 객체의 위치를 조절하는데, Time.deltaTime을 곱해주면 프레임 간의 간격을 고려하여 일관된 속도로 이동하도록 만듭니다. 이렇게 함으로써 게임의 속도는 사용자의 컴퓨터 성능이나 환경에 관계없이 일정하게 유지됩니다.
[SerializeField] private float speed = 5f;
[SerializeField] 란?

(feat. Chat.GPT)
Unity 게임 엔진에서 사용되는 어트리뷰트(Attribute) 중 하나입니다. 이 어트리뷰트는 private으로 선언된 변수를 Inspector 창에서도 접근 가능하게 만들어주는 역할을 합니다.
일반적으로 C#에서 private으로 선언된 변수는 클래스 내부에서만 접근이 가능하며, Inspector 창에서는 보이지 않습니다. 그러나 [SerializeField]를 사용하면 private 변수도 Inspector 창에서 값을 설정할 수 있게 됩니다.
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public event Action OnAttackEvent;
event는 외부에서는 호출하지 못하게 막는다

 

Window > Package Manager > Unity Registry > Input System(install)

Input폴더 생성 > 클릭한 상태로 Create > inpute Actions

키보드와 마우스 설정을 해준다.

public void OnMove(InputValue value)
{

}

public void OnLook(InputValue value)
{

}

public void OnFire(InputValue value)
{

}

센드 메시지 방식!
Move, Look, Fire 액션 앞에 On을 붙치면 그 애들이 실행됐을 때 돌려받는 함수를 만들어 주는 것
Player > Plyaer Input > Behavoir > Send Messages(OnMove, OnLook, OnFire라는 이름이 있으면 실행을 시켜주겠다.)
    public void OnMove(InputValue value)
    {
        Vector2 moveInput = value.Get<Vector2>().normalized;
        CallMoveEvent(moveInput);
    }
normalized을 하는 이유?
오른쪽 방향키와 위쪽 방향키을 동시에 누를 때 normalized을 안해주면 직선 방향은 느린데 대각선 방향은 빨라진다.

public void OnLook(InputValue value)
{
    // Debug.Log("OnLook" + value.ToString());
    Vector2 newAim = value.Get<Vector2>();
    Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);
    newAim = (worldPos - (Vector2)transform.position).normalized;

    if (newAim.magnitude >= .9f)
    {
        CallLookEvent(newAim);
    }
}
이 코드는 주어진 InputValue를 기반으로 플레이어가 어떻게 화면을 봐야 하는지를 처리하는 부분이다

(feat. Chat.GPT)
OnLook 메서드는 InputValue를 매개변수로 받습니다.
InputValue에서 Vector2 값을 추출합니다. 이 벡터는 플레이어의 입력에 따라 화면의 어떤 부분을 봐야 하는지를 나타냅니다.
_camera.ScreenToWorldPoint(newAim)을 사용하여 화면 좌표를 월드 좌표로 변환합니다. 이렇게 하면 플레이어가 화면을 어떻게 보는지에 대한 월드 좌표가 얻어집니다.
플레이어의 위치를 기준으로 새로 얻은 월드 좌표에서 플레이어를 향하는 단위 벡터를 계산합니다.
만약 계산된 단위 벡터의 크기가 0.9 이상이라면 (newAim.magnitude >= .9f), CallLookEvent 메서드를 호출하여 새로 계산한 단위 벡터를 이벤트로 보냅니다.
    private void Awake()
    {
        _controller = GetComponent<TownCharacterController>();
        _rigidbody = GetComponent<Rigidbody2D>();
    }
GetComponent란?
TopDownMovement 달려있는 같은 오브젝트에서 찾아오겠다.
인스팩터안에서 각 컨퍼넌트들 끼리 서로와 서로를 인지 할 수있는 방법

(feat. Chat.GPT)
Unity에서 사용되는 메서드로, 게임 오브젝트에 부착된 컴포넌트(Component)를 가져오는 데 사용됩니다. Unity에서 게임 오브젝트는 여러 가지 동작과 속성을 가지는데, 이를 구현한 것이 컴포넌트입니다. GetComponent를 사용하여 특정 컴포넌트의 인스턴스를 가져와서 사용할 수 있습니다.

 

window > 2D > Tile Palette

Collision (충돌체) : 나가지 못하는 벽면 만들기

> Tilemap Collider 2D 달아주기

> Tilemap > Color A값을 0

게임 결과물!

 

맵을 구현에 꾸며보았고 캐릭터가 이동 가능하며 화살을 들고 있고 벽에 닻아도 벽 밖으로 안 나가지는 거까지 구현하였다!

 

오늘의 오류

분명 스크립트에 오류는 없는데 유니티에 이런 오류가 뜬다

error : 'TopDownCharacterController' does not contain a definition for 'OnAttackEvent' and no accessible extension method 'OnAttackEvent' accepting a first argument of type 'TopDownCharacterController' could be found (are you missing a using directive or an assembly reference?)
error : The name 'IsAttacking' does not exist in the current context
(대충 ~ 대한 정의가 없으며 ~찾을 수 없다.
현재 컨텍스트에 'IsAttacking' 이름이 없다는 뜻)

한참을 고민하고 고쳐보았지만 답이 안 보였다..
결론 마지막 수단! 유니티가 문제인가?? C# 스크립트에 있는 코드를 모두 복사를 하고 유니티 C# 스크립트을 모두 삭제한 후 다시 만들어 복사해둔 코드를 붙여넣기 할려고 했다.
근데!!!! C# 스크립트을 닫으려고 할 때 저장하시겠습니까?? 라는 문구가 떳고 무심코 저장을 눌렀는데.............................오류가 사라졌다...ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
역대급 현타가 왔다..... 문제는 코드는 열심히 작성해놓고 저장을 안해서 적용이 안된 것...
오늘의 교훈 저장을 생활화 하자!!^^