7강입니다 constructor 01

생성자와 소멸자에 대해 설명합니다.

sturct 를 클래스로 변경했습니다. class로 변환 시 안에 모든 데이터 멤버들을 public으로 전환해줘야합니다. 이렇게 변환 시 바꾸기 전 sturct와 유사하게 변환됩니다. 

#include <iostream>



class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};

void main() 
{
	KPeople a;
	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
}

 

 

클래스를 설계하다 보면 객체를 만들자 마자 초기화를 해 줄 필요가 있습니다. 위 코드에선 age, name을 초기화 할 필요가 있다고 과정해보겠습니다. 초기화하지 않은 변수를 접근하는걸 위험하기 떄문입니다.

그리고 어떤 이유에서 객체를 파기하기전에 마무리 해줄 필요가 있다면 멤버 함수로 정의 할 수 있습니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	void Initialize() 
	{
		age = 0;
		name[0] = 0;
	}

	void Finalize() 
	{
		// 마지막에 해체 전 실행될 멤버 함수
	}

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};

위와 같이 멤버 함수의 Initialize와 Finalize를 만들어주고 사용하게 되면 아래와 같습니다.

void main() 
{
	KPeople a;
	a.Initialize();

	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
	
	a.Finalize();
}

따로 메인에서 초기화를 안할 시 쓰레기값이 들어가 잘못된 결과값이 날 올 수 있기때문에 외에도 여러 상황에서도 사용 가능합니다. 이와 같이 객체를 만들자 마자 초기화하는 함수를 생성자라고합니다. 반드시 자기 자신과 똑같은 이름을 가져야합니다. 객체를 만들자마자 자동으로 호출되기때문에 함수 호출 이름을 명시 할 방법이 없습니다. 함수가 종료되기전에 Finalize도 자동으로 명시 할 방법 또한 없습니다. 그래서 특별한 이름을 가져야합니다.

그래서 C++에서는 객체를 만들자 마자 자동으로 호출되는 초기화 함수를 생성자라 합니다.

클래스의 생성자를 정의하는 방법은 아래와 같습니다.

KPeople() //constructor
{
  age = 0;
  name[0] = 0;
  printf("constructor()\r\n");
}

소멸자 함수는 특별한 문법을 따라줘여합니다. 그리고 소멸자는 객체가 파괴되기 바로 전에 호출되는 함수입니다.

~KPeople() //destructor
{
	printf("constructor()\r\n");
}

이렇게 하는 경우 이제는 자동으로 호출되므로 따로 생성자, 소멸자를 만들고 main에 올릴 필요 없습니다.

void main() 
{
	KPeople a;
	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
}

이제 KPeople을 동적할당 해보겠습니다.

	KPeople* p;
	p = (KPeople*)malloc(sizeof(KPeople));
    
	free(p);

malloc이 리턴하는 값은 보이드 포인터이기 때문에 캐스팅을 해줘야합니다. (KPeople*) 그리고 사이즈는 KPeople만큼 필요하므로 KPeople을 넣어줍니다. 그리고 메모리 해제 또한 해줘야합니다. 이렇게 될 시 이제 p를 통해서 접근이 가능하게됩니다.

요기서 빌드 시 쓰레기값이 나옵니다. 생성자에서 초기화를 해줬지만 쓰레기 값이 나오는 이유는 메모리 동적 할당 시 생성자를 자동 호출해주지 않습니다. 그래서 p를 이용해서 생성자를 호출해주는 부분을 직접 작성해줘야합니다. 소멸자도 마찬가지 입니다.

void main() 
{
	KPeople* p;
	p = (KPeople*)malloc(sizeof(KPeople));
	p->KPeople::KPeople();

	p->~KPeople();
	free(p);
}

KPeople만 입력 시 클래스 이름과 같아 모호함이 발생하므로 KPeople에 생성자를 호출한다고 명시해줍니다. 소멸자 경우 ~가 붙어있어 상관없습니다.

하지만 전에도 말했듯이 C++에선 new와 delete를 사용합니다.

	KPeople* p;
	p = (KPeople*)operator new (sizeof(KPeople)); // new 함수 ( new function)
	p->KPeople::KPeople();

	p->~KPeople();
	operator delete(p);

operator new 또한 보이스 포인터이므로 (KPeople*)로 캐스팅 해줍니다. operator new 경우 전역함수라 생각하면됩니다. operator new를 함수 이름이라고 생각하는게 나중에 헷갈리지 않는다고 합니다. 

하지만 이또한 생성자를 따로 호출해줘야합니다. 하지만 C++ 설계자들이 동적으로 할당 하고 자동으로 생성자를 호출 하도록 operator new와 생성자를 호출하도록 new expression을 만들었습니다 .

	p = new KPeople;
    
	delete p;

이것의 의미는 동적으로 KPeople만큼 메모리에 할당하고 자동으로 생성자 자동으로 호출하라는 뜻입니다.

소멸자 경우도 마찬가지 입니다. p의 경우는 KPeople 포인터입니다.

하지만 그럼 operator new는 필요없는거 아니냐 할 수 있지만 new expression은 오버로딩이 불가합니다 그래서 new의 오버로딩이 필요한 경우 operatorn new로 오버로딩을 해야합니다. 하지만 동적으로 메모리를 할당할 일이 있다면 new expression, delete expression을 사용하면 됩니다.

8강입니다.

 생성자의 파라미터가 들어갈 시 () 안에 들어갑니다. 없을 시 생략해도됩니다. 있어도 생략안하고 적어도 작동합니다.

p = new KPeople();

이런 식으로 처리 가능합니다.

p = new KPeople[3];

연속적으로 메모리 할당이 가능합니다. 뜻은 KPeople의 크기의 메모리를 3개를 할당한것입니다. new 에 []를 사용 시 delete[] p 와 같이 딜리트에도 표시해줘야합니다. 물론 3개를 만들기때문에 생성자 3번 소멸자 3번이 작동합니다.

그리고 접근 시엔 아래와 같이 접근 가능합니다. 0 , 1, 2 등 직접적인 접근이기 때문에 포인터가 아닌 객체가 되므로 . 을 사용합니다.

p[0].PrintPeople();
p[1].PrintPeople();
p[2].PrintPeople();

그리고 오버로딩을 생성자를 오버로딩 할 수 있습니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	KPeople() //constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}
	KPeople(const char* name_) // overloading
	{
		age = 0;
		strcpy(name, name_);
		printf("constructor(char*)\r\n");
	}

	~KPeople() //destructor
	{
		printf("destructor()\r\n");
	}

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};
// 1번째 생성자
KPeople p;

// 2번째 생성자
KPeople p("John");
KPeople p = "John"; // 암시적인 생성자 호출 implicit construton call

 

void Test( KPeople p )
{
	p.PrintPeople();
}

void main()
{
	KPeople p = "John";
	Test("Mike");
}

암시적 생성자 호출을 할 시 이런식으로 사용 가능하지만 이렇게 사용 할 시 문제가 될 수 있습니다. 그래서 생성자 호출을 할 때 명시적인 호출만 받겠다라고 지정할 수 있습니다.

explicit KPeople(const char* name_)
{
  age = 0;
  strcpy(name, name_);
  printf("constructor(char*)\r\n");
}

생성자에 explicit을 앞에 붙여주면 됩니다. 이렇게 될 시 암시적 호출을 할 시 컴파일 에러를 발생 시킵니다. 이렇게 바꾸고 위위 코드에서 컴파일 할 시 오류를 발생하는걸 볼 수 있습니다. 그래서 사용을 하려면 아래와 같이 수정합니다.

void main()
{
	KPeople p ("John"); // implicit construction call
	
    	KPeople temp = KPeople("Mike");
	Test(KPeople("Mike")); //temporary object
}

KPeople temp에 생성자로 KPeople("Mike")를 전달 시키는 겁니다.

Test 함수에 직접 단독으로 쓰인 경우는 KPeople("Mike")를 KPeople타입에 객체 파라미터로 "Mike"를 전달하고 임시 스택에 만들어지는 임시 객체를 생성한다는 뜻입니다. 그래서 이문장은 생성자를 호출합니다. 

근데 이런식으로 코드를 짠 후 컴파일 해서 결과를 보면 생성자 3번 소멸자가 4번이 호출되는 현상이 생깁니다.

9강입니다.

위와 같이 할 시 생성자가 p , temp, KPeople , Test 에서의 KPeople p 에서 생성자가 총 4번 생성 소멸자가 똑같이 4번 생성되어야하지만 생성자가 3번만 생성됩니다. 왜 그런지 메모리를 그려보겠습니다.

정리도 할겸 천천히 메모리를 그려가며 내려가겠습니다. 항상 주소는 임의에 값을 적어논겁니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];
    
void main() 
{
	KPeople p("John");

메인에 들어왔을때 p가 할당됩니다. p는 KPeople 타입으로 int 4바이트 char[12] 12바이트 총 16바이트를 차지합니다. name의 시작주소는 5004 +4된 값이 됩니다. 5000+15까지 차지하는 16바이트의 크기의 메모리가 됩니다. 

	KPeople() //default constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}
	explicit KPeople(const char* name_)
	{
		age = 0;
		strcpy(name, name_);
		printf("constructor(char*)\r\n");
	}

"John" 전달합니다. 그렇게되면 2번째 생성자가 호출되어서 name을 복사하고 age를 0으로 만듭니다.

이렇게 들어옵니다. 하지만 age랑 name이라는 값은 이곳에 표기되어있지 않습니다. 혼동을 피하기 위해서 지워줍니다. 그리고 이제 temp 객체를 만듭니다.

KPeople temp = KPeople("John");

Temp를 만들었습니다 . 다음 KPeople("Mike")를 임시로 호출합니다.

Test(KPeople("Mike")); //temporary object

이 객체는 어디로 만들어질지 모릅니다. 실제로 스택에 만들어질 확률이 높습니다. 주소와 이름이 없습니다. 그래서 temporary object 라 부릅니다.

이렇게 생성자가 총 3번 호출됐습니다. 임시로 만들어진 객체를 이제 Test에 전달됩니다.

void Test(KPeople p) 
{
	p.PrintPeople();
}

파라미터로 KPeople p를 메모리에 할당합니다. 요기서 생성자가 3번만 호출된 이유를 알 수 있습니다. Test 함수를 진행하며 KPeople p가 생성된게 아닌 임시객체로 생성된 ?를 그대로 복사한 형태여서 Test 함수에선 생성자를 호출하지 않습니다.

이렇게 Test 함수를 작동하면 출력하고 일을 끝낸 Test 함수를 메모리 해제하며 그전에 소멸자가 호출됩니다.

그리고 이제 main으로 돌아가 스택메모리에서 순서대로 메모리 해제 전에 소멸자가 호출됩니다. 그래서 총 4번 호출이 됩니다.

생성자와 소멸자가 쌍으로 안이루어질때 오류가 발생할 수 있습니다. 그럼 근본적인 문제인 객체가 복사가 되는 경우에 호출되는 특별한 생성자를 정의를 해줘야합니다. 이걸 copy constructor라고 합니다.

copy constructor는 객체가 복사되는 문법을 표현해주기 위해서 자기 자신의 타입에 레퍼런스(&)를 매개변수로 받습니다. 복사되는 시점에 다시 복사를 막기 위해서 입니다. 

KPeople(const KPeople& rhs) // copy constructor
	{
		printf("copy constructor()\r\n");
	}

문법 기억

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	KPeople() //default constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}

	explicit KPeople(const char* name_)
	{
		age = 0;
		strcpy_s(name, name_);
		printf("constructor(char*)\r\n");
	}

	KPeople(const KPeople& rhs) // copy constructor
	{
		age = rhs.age;
		strcpy_s(name, rhs.name);
		printf("copy constructor()\r\n");
	}
	~KPeople() //destructor
	{
		printf("destructor()\r\n");
	}
};

void Test(KPeople p) 
{
	p.PrintPeople();
}

void main() 
{
	KPeople p("John"); // 암시적인 생성자 호출 implicit construton call
	KPeople temp = KPeople("John");
	Test(temp); //temporary object
}

오류 시 strcpy_s를 사용합니다. 요기서 주의 해야할 점은 이미 복사한 값을 호출 할 시 복사 생성자가 호출된다라는 점입니다.

Test(temp)로 변경 이유는 KPeople("Mike")를 작성 시 생성자 한번 복사 생성자 한번이 호출이 되는데 컴파일 최적화로 생성자 한번만 호출되어 복사생성자를 호출하지 않는 문제가 있어서 이미 복사된 값을 사용했습니다. 위 메모리를 그리며 정리한걸 봐도 ?를 Test로 복사한 형태라서 생성 후 복사 호출되는게 맞지만 최적화된것 같습니다. 

객체가 복사되는 시점과 생성되는 시점을 이해하면 좋습니다.

10강입니다. 

객체를 만드는 팩토리 함수를 만듭니다.

KPeople GetPeople()
{
	KPeople t;
	return t;
}
or
KPeople GetPeople()
{
	return KPeople("Mary"); //RVO
}


void main() 
{
	KPeople t;
	t = GetPeople();
	//Test(t); // temporary object
}

 

생성자가 2번 작동합니다. 하지만 복사생성자가 작동하지 않는건 아닙니다. class 내에서 private 처리해버리면 오류가 발생합니다. 이유는 RVO에 의해 스킵될뿐 복사생성자를 검사를 하지 않는게 아니기 때문입니다.

RVO는 컴파일 최적화 형식으로 생성자를 최소화 합니다.

'프로그래밍 공부 > C++' 카테고리의 다른 글

C++ (1)  (0) 2021.07.07

C++ 강좌를 보면서 배운걸 정리합니다.

유니티 포트폴리오로는 C++ 회사에 지원하기 힘든 것 같아 언리얼 엔진4을 만져보기 위해 공부하는 중입니다.

포트폴리오로 만들기 위해서 어떤 방향으로 해야할지는 C++ 공부 후 검토해볼 생각입니다.

참고 강좌 : C++

struct 강좌는 총 3강입니다.

사용할 구조체 형태입니다.

struct People // type definition
{
	int age; // member variable
	char name[10];
}/*a*/; 

People 부분은 태그네임이라고 합니다. struct People이라는 타입으로 정의된 동시에 a라는 변수로 정의된 상태입니다. 구조체는 타입도 정의할 뿐만 아니라 변수도 정의할 수 있는 특별한 구조를 가지고 있습니다.

그리고 구조체로 타입을 정의 할때 변수를 정의 할 수 있지만 가독성을 위해 변수를 바로 선언하지 않고 이후 사용 할때 다시 변수로 선언하여 사용합니다.

구조체를 사용 시 

void main ()
{
    People a;
    a.age = 28;
    strcpy_s(a.name, "Hello");

    //strut People b; strut People이 타입 이름
    People b; // strut 생략가능

    b.age = 29;
    strcpy_s(b.name, "World");
}

strut에 경우 C에선 생략이 불가능합니다. strcpy_s에 경우 문자열을 복사하는 함수입니다. 

이렇게 작성 후 출력을 해보겠습니다.

void PrintPeople(People A) {
	printf("%s : %i\r\n", a.name, a.age);
}

PrintPeople(a);
PrintPeople(b);

결과 값

Hello : 28

World : 29

 

2강 정리입니다. 아래는 전에 코드를 정리했습니다.

struct People // type definition
{
	int age; // member variable
	char name[10];
};

void main()
{
	People a;
	People b;
	 
	a.age = 47;
	strcpy_s(a.name, "Hello");

	b.age = 48;
	strcpy_s(b.name, "World");

	PrintPeople(a);
 	PrintPeople(b);
}

구조체를 main의 스택 메모리를 그리면 아래 그림이 됩니다.

내부 단편화 - 표기된건 10바이트지만 윈도우에서 효율적인 메모리 관리를 위해 4바이트씩 끊어서 사용 
그래서 실제로 12바이트를 차지하고 마지막 2바이트가 비어있게 됩니다.

age 부분은 주소값이지만 편의를 위해 age로 표기했습니다.

실제로 위와 같은 구조를 가졌는지 테스트를 해보도록 하겠습니다.

	char *cp = (char*)&a;

캐스팅(형변환)을 한 이유는 &a의 시작 주소는 구조체의 시작 주소이므로 char포인터에 대입이 불가해서 캐스팅을 해줬습니다.

 

그리고 테스트를 위해 e를 출력해보겠습니다.

 위 그림 처럼 메모리를 그린게 맞다면 메모리 위치를 계산 시 +5를 할 시 e를 찾을 수 있습니다.

	printf("%c\r\n", cp[5]);

*(cp + 5) 브레킷연산자를 사용해 cp[5]로 출력하면 e가 나옵니다.

void main()
{
	People a;
	People b;
	 
	a.age = 27;
	strcpy_s(a.name, "Hello");
	char *cp = (char*)&a;
	printf("%c\r\n", cp[5]);

	b.age = 46;
	strcpy_s(b.name, "World");

	PrintPeople(a);
}

 이 상황에서 PrintPeople 함수를 호출 시 

PrintPeople 만큼의 메모리를 할당합니다.

메모리 몇번째인지는 함수호출이 일어나면서 중간에 메모리를 차지하므로 메모리 정확한 계산은 힘듭니다.

프린터피플의 스택프레임, 메인 스택 프레임을 나눠지며 메인에서 정의한 a 값이 프린트피플에서 할당한 A 공간으로 복사가 됩니다.

위와 같이 되므로 프린터피플에서 a.age의 값을 바꿔도 값은 변경되지 않습니다. 단순하게 복사한 값이기 때문입니다. 이렇게 함수 안에서 변경된 값은 함수 밖 값에 영향을 줄 수 없습니다. 이걸 함수에서 변경을 하기 위해선 포인터를 전달해야합니다.

3강 정리입니다.

void PrintPeople(People a) {
	printf("%s : %i\r\n", a.name, a.age);
	++a.age;
}

포인터를 사용하기전 테스트를 해봤습니다. 위 함수에 ++a.age를 추가합니다.

함수를 실행 시 PrintPeople 프레임 age에 +1이 늘어나지만 함수를 빠져나오는 PrintPeople 값이 팝되어서 main 값엔 영향이 없습니다.

a를 변경되지 않고 +1 계산된 값을 출력하고 싶은거면 맞지만 의도가 PrintPeople에서 계산값된 값을 main에 있는 age 값을 변경하고 싶으면 위와 같이 구조체를 전달해서는 바끼지 않습니다.

 ++a.age부분을 제거 후 다시 함수를 만듭니다.

void SetAge(People *A, int newAge) 
{
	(*A).age = newAge;
}

void main()
{
	SetAge( &a, a.age + 1 );
	PrintPeople(a);
}

필요한 부분만 적었습니다. a = 47입니다.

SetAge는 매개변수로 들어온 구조체(A).age를 newAge로 들어온 값으로 바꿔주는 역할을 합니다.

원래 함수는 값의 변경이 안되었는데 이번 함수의 다른 점은 변경을 필요로 한 값을 포인터로 넘겨주는겁니다. 메모리로 가서 직접 수정을 하는것이죠. 그리고 코드상에선 함수에게 줄 땐 & 주소연산자로 넘겨주고 함수에선 * 참조연산자로 받습니다. 이렇게 받으면 A는 현재 구조체 a를 복사한게아닌 구조체 a를 가르키는 포인터입니다. 그래서 (*A)를 사용했습니다. 이렇게 한 이유는 A는 더이상 구조체가 아니기 때문에 . 을 사용할 수 없습니다.

그래서 (*A) *A포인터가 가르키는 내용이 이게 구조체가 됩니다.

그리고 그값에 newAge를 대입하고 SetAge 함수를 빠져나옵니다. 그러면 SetAge부분이 스택에서 팝됩니다.

그리고 PrintPeople( a ) 부분이 실행되면서 함수가 호출됩니다. 이때 SetAge에 의해서 a.age는 47에서 48로 대입된 상태입니다. 48이 출력됩니다. 

메모리 또한 위에서 본것과 같이 a 구조체를 복사 스택에 push 출력 후 pop됩니다. 그리고 메인으로 돌아옵니다.

해보고 알 수 있는건 포인터로 전달 시 값에 대해서 변경 가능한 점과 포인터 경우 4바이트만 사용하여 메모리를 효율좋게 사용 할 수 있습니다.

void PrintPeople(People *A)
{
	printf("%s : %i\r\n", (*A).name, (*A).age);
}

// main 부분

	PrintPeople(&a);

구조체가 클 경우 단순 복사만 해도 메모리에 부담이 갑니다. 그래서 포인터로 메모리 최적화를 요 할 수 있습니다.

4강입니다.

구조체에 접근 하기 위해선 . 을 사용합니다. 하지만 구조체 포인터일 때는 People *A;에 A는 구조체가 아닌 포인터입니다. 그래서 *A가 가르키는 내용이 구조체입니다. 포인터는 가르키는것이기 때문에 연산전에는 포인터인채이고 연산자 우선 순위로 인해 먼저 계산이 안될 수 있으므로 (*A)로 표기해줍니다. 하지만 구조체를 메모리 효율을 위해 포인터를 사용하는 일이 많습니다. 매번 (*A)를 적긴 많이 번거롭습니다. 그래서 만들어진게 arrow operator 이며 dot operator 도 있습니다. 둘다 구조체를 위한 오버레이터입니다. dot operator는 구조체 자체가 선언되었을때 구조체 멤버로 접근합니다. arrow operator는 구조체 포인터로 선언되었을때 구조체 멤버로 접근합니다. 요기서 dot operator는 이때까지 써온 . 이란걸 알 수 있습니다. 그럼 arrow operator는 어떻게 사용하냐면 (*A).age ===> A -> age , * . 가 -> 로 바뀐 형식입니다.

void PrintPeople(People *A) {
	printf("%s : %i\r\n", A->name, A->age);
}

void SetAge(People *A, int newAge) 
{
	A->age = newAge;
}

arrow operator 로 수정한 함수 코드입니다.  - > 띄어씌기는 안됨

	People *b;
    
	b -> age = 48;
	strcpy_s(b -> name, "World");
    
   	PrintPeople(b);

이런식의 예제도 만들 수 있습니다. 구조체 변수로 생성하지 않고 포인터로 바로 생성 시는 dot이 아닌 arrow operator로 표기하며 포인터를 받는 함수의 경우 & 주소연산자를 사용하지말고 바로 포인터인 b를 넘겨주면 됩니다.

하지만 요기서 오류가 발생합니다. 이유는 그림으로 보면 이렇습니다.

b -> age = 48; 대입을 하는 순간 할당되어있지 않은 메모리를 사용해서 에러가 발생하는 상태입니다. 그래서 구조체가 가르킬 곳을 메모리를 할당한 다음 사용해줘야합니다.

void main()
{
	People *b;

	b = (People*)malloc(sizeof(People));

	b -> age = 48;
	strcpy_s(b -> name, "World");

	SetAge( b, b->age + 1 );
	PrintPeople(b);

	free(b);
}

malloc으로 가르키고 있는 곳에 메모리를 할당 해주고 sizeof 해서 사이즈를 정해줍니다. 하지만 이렇게 하면 void리턴이라서 오류가 납니다. malloc은 자동적으로 자료형을 정해주지 않아서 캐스팅을 해주어야합니다. 그래서 (People*) 를 적어줍니다.

코드를 보면 포인터는 따로 초기화 하지 않아 갈비지로 초기화 되어있습니다. 그리고 malloc으로 People (14) 사이즈 만큼 할당 됩니다. 위치는 알 수 없습니다. 임의로 정하겠습니다. malloc을 쓰고 나선 항상 free()를 사용해줍니다. 스택 메모리 처럼 그려놨지만 malloc이나 new 같은 동적 할당 메모리는 크기를 넓게 쓰기 위해 만들어진 곳이라서 heep 메모리에 저장됩니다. 

	b -> age = 48;
	strcpy_s(b -> name, "World");

 

위에 값을 기입하게 됩니다.  그 후에 

	SetAge( b, b->age + 1 );
	PrintPeople(b);

그 후에 함수를 실행하게 될 시 b 이미 포인터인 녀석을 넘겨주게 됩니다. 포인터 b를 넘겨주고 b를 구조체화하여 age 값에 +1 하여 함수를 계산합니다.

그후 출력합니다. 또한 밑에 함수도 정상 출력됩니다.

5강입니다.

메모리 계산 부분에서 실수를 해서 그림을 다시 고쳤습니다.

메모리 할당을 malloc로 하고 free로 했었는데 C++에선 같은 역할을 하지만 다른 형식이 있습니다. new 와 delete입니다. new는 메모리 할당을 하고 생산자 Constructro 라는 특수한 함수를 호출하고 delete는 메모리 해제를 하기 전에 파괴자라는 특별한 함수를 호출 합니다.

약간 다릅니다. 다른 부분은 나중에 따로 포스팅하겠습니다.

C++의 구조체 대해서 알아보겠습니다. C++ 구조체는 구조체 안에 함수를 가질 수 있습니다.

struct People // type definition
{
	int age; // member variable
	char name[10];  
	
	void PrintPeople(People *this_) { // member function
		printf("%s : %i\r\n", this_->name, this_->age);
	}

	void SetAge(People * this_, int newAge)
	{
		this_->age = newAge;
	}
};

특이한 형태입니다 구조체안에 들어간 함수는 이제 함수라고 하지 않고 멤버 함수라고 합니다.

호출 방법은 구조체에 접근 후 멤버함수를 호출하면됩니다.

6강입니다.

위와 같이 사용하게 될 시 위 함수를 보면 매개변수로 this_ 라는 포인터를 따로 main에서 받아 함수를 실행하는데 구조체에 멤버함수가 포함되어있는 구조체에서 멤버함수를 사용 시 구조체를 매개변수로 받을 일이 있을 시 매개변수가 아닌 함수내에서 this로 사용할 수 있습니다. 위와 같이 멤버함수는 항상 this 자기 자신을 항상 매개변수로 전달하는데 이걸 this 콜이라고 합니다.

	void PrintPeople()  // member function
	{
		printf("%s : %i\r\n", this->name, this->age);
	}

	void SetAge(int newAge)
	{
		this->age = newAge;
	}

 

멤버함수를 수정한 코드입니다. 그래서 PrintPeople은 전달하는 인자가 0개가 아닌 1개입니다.

그리고 5강을 통해 C의 함수와 C++의 함수가 다르는걸 알 수 있었습니다. 실제로 오버로딩때문에 C++에서 함수를 정의하면 함수이름 그대로 유지하지 않고 컴파일러가 변경합니다. 이걸 네임 맹글링 (Name Mangling)이라고 합니다. 그리고 멤버함수 역시 그냥 함수와 다릅니다. 자기 자신인 객체의 시작주소가 항상 전달됩니다. 그리고 멤버 함수의 경우 this라고 적어되지만 보통 this->를 생략해서 사용한다고 합니다.

	void PrintPeople()  // member function
	{
		printf("%s : %i\r\n", name, age);
	}

	void SetAge(int newAge)
	{
		age = newAge;
	}

 

void main()
{
	People *b;

	b = new People;

	b -> age = 48;
	// C 호출 관례 
	// C++ 호출 관례
	strcpy_s(b -> name, "World");
    
	// this 호출 관례
	b->SetAge( b->age + 1 );
	b->PrintPeople();

	delete b;
}

등 구별이 가능합니다. 함수 호출 관례에 대해선 나중에 따로 다루겠습니다.

그래서 이렇게 변수와 함수를 다 가진 구조체를 class라고 합니다. 그래서 struct를 class로 바꿔주면 됩니다. 하지만 접근 권한 문제가 발생하는데 아래와 같이 수정하면 됩니다.

class People // type definition
{
public:
	int age; // member variable
	char name[10];

	void PrintPeople()  // member function
	{
		printf("%s : %i\r\n", name, age);
	}

	void SetAge(int newAge)
	{
		age = newAge;
	}
};

링크드 리스트를 작성하기 위해서는 구조체 포인터를 자기자신의 멤버로 사용해야합니다.

그리고 구조체 멤버를 int 같은 타입이 아니라 특정한 몇 비트를 차지하도록 비트 필드 구조체를 만들수도 있습니다. 또 공용체는 구조체는 멤버를 위해서 모두가 메모리가 할당됩니다. 이런 멤버들 중에서는 가장 큰 멤버를 위해서만 메모리 할당이 됩니다. 하지만 이런것들은 나중에 따로 알아보고 지금은 문법에 집중하겠습니다.

'프로그래밍 공부 > C++' 카테고리의 다른 글

C++ (2)  (0) 2021.07.08

코딩 테스트 치고 나오면서 C++에 대한 기초가 부족한거같아서 C++을 한번 완전 기초부터 다시 해보려고 합니다. 그리고 테스트 준비하면서 공부한걸 다시 정리해서 올릴 생각입니다. 공부도 먼저 책으로 공부 한다고 포스팅은 공부를 다하고 정리하면서 적습니다. 너무 포스팅을 미뤘네요.

C++ 기초전에 C 부분과 기초이론부분을 좀 더 공부하고 왔습니다.

계산적 사고 Computational Thinking

입력 - > 알고리즘 - > 출력 (와 같은 형식은 다 알고리즘이라고 할 수 있지만)

자신이 원하는 답을 도출해낼 수 있는 알고리즘을 만들어야합니다. 문제를 해결 할 수 있는 단계를 찾는 행위

컴퓨터의 작동원리

컴퓨터 (정보화기기) - 하드, 소프트

구성 요소(참고)

중앙 기억 장치 CPU

주 기억 장치 RAM - 전원을 끄면 데이터가 사라짐 'Random' - Aceess Memory (RAM) 임의 접근이 가능한 메모리

그래픽카드 GPU

보조기억장치 - HDD, SSD

컴퓨터를 켤때 일어나는 일 (자료)

운영체제가 해주는 일 (참고)

UNIX, Linux

iOS, 안드로이드

MacOS

Windows

등이 해주는 일 

시스템 프로그램 ( 바탕화면 보호기, 바꿔주는 기능 등)

원래 하드웨어를 어떻게 사용할 것인지 운영체제에 커널(핵심)에서 담당을 한다.

커널 - 운영체제에서 하드웨어를 관리, 중계해주는 핵심 역할

리소스 관리도 운영체제

이후 영상은 전에 한번 정리한적 있어서 스킵하겠습니다. (참고)

영상의 핵심은 CPU, 메모리가, 입출력이 분리가 되어있고 System bus에서 입출력을 받아 나눠주는 일을 한다.

그중 Address bus는 데이터값이 아닌 데이터값의 주소 포인터가 존재하는 이유입니다

CPU 구성 요소 (참고)

제어 장치 Control Unit , 산술 논리 장치(ALU) Arithmetic Logic Unit , 그리고 갖가지 레지스터로 구성 되어 있습니다.

레지스터 Registers는 CPU에 책상역할을 할 수 있다고 볼 수 있습니다. 여러 값을 올려놓는 역할을 합니다.

시각적으로 표현하면 이렇습니다.

포인터는 정확히 주소 값을 가르키는 것입니다. 메모리 값이 아닌 이점을 유의해야합니다.

CPU가 작동하는 원리는 영상에 잘 설명되어있지만 한번 제가 따로 순서를 적은걸 말로 풀어보겠습니다.

1. 프로그램 카운터에 주소값이 들어옵니다. (시작점) 5. 다음 카운터 주소값이 들어옵니다.

2. 메모리 주소 레지스터로 주소값을 넘겨주고 3. 메모리 데이터 레지스터에 주소에 입력되어 있는 값이 들어옵니다.

4. 값이 명령어 일 시 명령어 레지스터로 들어갑니다. 6. 명령어 레지스터에 있는 값이 제어장치로 들어갑니다.

7. 디코드를 실행해서 명령어를 해독해서 실행합니다. (주소 10에 있는 주소를 로드해라) 8. 메모리 주소 레지스터에 10을 넣어줍니다. 9. 3을 메모리 데이터로 가져옵니다.  10. 연산에 사용되는 값은 어큐뮬레이터에 저장된다. 산술 연산 장치를 보조해주는 레지스터 11. 다음 카운터 주소값을 실행합니다. ( 명령이 끝날 때까지 반복하며 명령어를 실행해서 산술 논리 장치에 넣어줍니다. ALU에서 계산된 값을 102 주소에 명령에 따라 12주소에 저장해서 끝납니다.

정보의 기본 단위 Bit 비트

바이트 ( 8개의 비트 ) 메모리 주소의 기본 단위

16, 32 비트 CPU가 데이터를 다루는 기본 단위 - word 워드 - 레지스터의 크기 (고정은 아님)

 

While문

~scanf가 가능해서 사용해봤습니다.

A+B - 5

    int a,b;
    while(~scanf("%d %d", &a,&b)){
        if(a+b != 0)
        printf("%d\n", a+b);
    }

A+B - 4

    int a,b;
    while(~scanf("%d %d", &a,&b)){
        printf("%d\n", a+b);
    }

더하기 사이클

    int num, firNum, secNum, sumNum, count, result;
    result = 0;
    count = 0;
    
    scanf("%d",&num);
    
    if (num < 10)
            num *= 10;

    result = num;

    while (1) {
         firNum = result / 10;
         secNum = result % 10;
         sumNum = firNum + secNum;
         result = (secNum * 10) + (sumNum % 10);
         count++;

         if (num == result)
             break;
    }
    printf("%d", count);

헷갈릴땐 변수 다시 정리하고 세분화

 

C++이 좋아서 다시 턴 C#은 심화로 들어가면 같이 작성해서 넣어보겠습니다.

구구단

#include <stdio.h>

int main(){
    int n;
    scanf("%d", &n);
    
    for(int i = 1; i <= 9; i++){
        printf("%d * %d = %d\n", n,i,n*i);
    } 
    return 0;
}

A+B - 3

int n,a,b;
scanf("%d", &n);
for(int i; i < n; i++){
  scanf("%d %d", &a,&b);
  printf("%d\n", a+b);
}

int n;
int sum = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
  sum += i;
   }
   printf("%d", sum);
   return 0;
}

빠른 A+B

    int t,a,b;
    scanf("%d", &t);
    for(int i = 1; i <= t; i++){
        scanf("%d %d", &a,&b);
        printf("%d\n" , a+b);

위 문제를 풀고 제출 같은 언어였습니다. 근데 시간 8ms을 보고 클릭 안할수가 없었습니다. 코드를 보고 정신이 아늑해졌습니다. 궁금하시면 문제 풀이 후 보면 됩니다. 속도순으로되어있어서 맨위에 있습니다. 코드를 보면 신기하면서 배우는게 많네요, 한번씩 다 봐야겠습니다.

N 찍기

    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        printf("%d\n", i);

기찍 N(리버스)

    int n,num;
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        num = n - i;
        printf("%d\n", num);
    }

A+B - 7

A+B - 8 

printf문 유희로 스킵하곘습니다.

별 찍기 - 1

    int n;
    scanf("%d" , &n);
    for (int i = 0; i < n; i++){
        for (int j = 0; j <= i; j++){
            printf("*");
        }
        printf("\n");

별 찍기 - 2

    int n;
    scanf("%d", &n);
for (int i = 1; i <= n; i++) {
        for (int j = 0; j < n-i; j++) {
            printf(" ");
        }
        for (int z = 0; z < i; z++) {
            printf("*");
        }
        printf("\n");

X보다 작은 수

#include <iostream>

using namespace std;

int main() {
    int length, maxNum;
    scanf("%d %d", &length, &maxNum);
    int arr[length];
    for(int j = 0; j < length; j++){
        scanf("%d", &arr[j]);
    }
    
    for(int i = 0; i < length; i++ ){
       if(maxNum > arr[i]){
           printf("%d ", arr[i]);
       }
    }
    return 0;
}

위 문제 다른 코드

#include<stdio.h>
int main(){
	int a,b,c;
	scanf("%d %d",&a,&b);
	for(int i= 0; i < a; i++){
		scanf("%d", &c);
		if(c < b) printf("%d ",c);
	}
	printf("\n");
}

for문 끝났습니다.

C++ 출력만 해보고 앞으로 C#으로 해보겠습니다. 유니티로 취업을 할거고 계속 사용해서 친해져야하는 언어이므로 앞으로 문제는 C#으로 풀어보겠습니다.

 

두 수 비교하기

using System;

class Program {
    static void Main(){
        string str;
        int a,b;
        
        str = Console.ReadLine();
        string[] strArr = str.Split(' ');
        a = Convert.ToInt32(strArr[0]);
        b = Convert.ToInt32(strArr[1]);
        
        if(a < b) 
            Console.WriteLine("<");
        else if(a > b) 
            Console.WriteLine(">");    
        else if(a == b) 
            Console.WriteLine("==");
        else
            Console.WriteLine("잘못된 입력 값입니다.");
    }
}

시험 성적

string str;
int score;

str = Console.ReadLine();
score = Convert.ToInt32(str);

if(score >= 90)
    Console.WriteLine("A");
else if(score >= 80)
	Console.WriteLine("B");
else if(score >= 70)
	Console.WriteLine("C");
else if(score >= 60)
	Console.WriteLine("D");
else
	Console.WriteLine("F");

윤년

string str;
int year;
int a = 4;
int b = 100;
int c = 400;
bool yearBool;

str = Console.ReadLine();
year = Convert.ToInt32(str);

yearBool = year%a == 0 && year%b != 0;

if(yearBool || year%c == 0)
	Console.WriteLine("1");
else
	Console.WriteLine("0");

사분면 고르기

	
    int a,b;

    a = int.Parse(Console.ReadLine());
    b = int.Parse(Console.ReadLine());

    if (a > 0 && b > 0)
        Console.WriteLine("1");
    else if(a < 0 && b > 0)
        Console.WriteLine("2");
    else if(a < 0 && b < 0)
        Console.WriteLine("3");
    else if(a > 0 && b < 0)
        Console.WriteLine("4");
    else
        Console.WriteLine("잘못 입력했습니다.");

Convert.ToInt32 보다 int.Parse 속도, 메모리 면에서 더 유리합니다.

알람 시계

        int h,m;
        int setM;
            
        string[] arrStr = Console.ReadLine().Split(" ");
        setM = 45;
        
        h = int.Parse(arrStr[0]);
        m = int.Parse(arrStr[1]);
        m -= setM;
        if (m < 0) {
            m = Math.Abs(m);
            m = 60 - m;
            h -= 1;
        }
        if(h < 0)
        {
            h = 23;
        } 
        Console.WriteLine("{0} {1}", h,m);

if문 완료했습니다.

 

'프로그래밍 공부 > 알고리즘' 카테고리의 다른 글

C++ 백준 while문  (0) 2021.06.10
Day1 백준 입출력과 사칙연산  (0) 2021.05.25
[알고리즘] C# ArrayList와 LinkedList  (0) 2021.05.15
[알고리즘] C# 배열 재정리  (0) 2021.05.15
[알고리즘] 자료구조?  (0) 2021.05.15

보스 부분은 아직 보완해야할 부분이 많아서 건너띄고 리팩토링 후 포스팅할 생각입니다.

그럼 그전에 셰이더 부분부터 보겠습니다.

계산을 하는 부분입니다. 앞전에 셰이더 튜토리얼에서 작성한 부분입니다.

기본 툰 셰이더는 텍스쳐로 값을 받아서 적용시켜야해서 기존 툰 셰이더보다 코드로 툰 셰이더를 작성해서 사용해보자라는 취지로 만들었습니다. 왼쪽이 기존 셰이더인 그림자를 적용 안한 모습입니다.

오른쪽은 어두운면엔 간접광인 엠비언트 라이트를 추가하고 그림자와 빛 사이 스무브 스텝(smoothstep) 함수를 사용하여 매끄럽게 블렌딩해줬습니다.

왼 : 기본 툰 셰이딩 (그림자 미적용) , 오른 : 셰이더 코드로 엠비언트 및 빛(그림자)를 적용 시킨 상태

다음은 정반사(Specular reflection)와 림 라이트 (Rim lighting)을 만들어서 적용 시켰습니다. 정반사는 보는 방향인 즉, 카메라가 보는 방향을 따라 움직이는 뚜렷한 빛반사입니다.

림 라이트는 물체에 가장자리에 조명을 추가해서 툰 셰이더에서 오브젝트를 돋보이게 하기 위해 사용했습니다. 카메라 시점에서 멀리 향하는 표면에 값을 구하는것이며 노말벡터와 뷰 방향의 내적을 취하고 그것을 반전시킨 값입니다.

이렇게 툰 셰이더를 완성시켜 적용시켰습니다.

팀프로젝트를 마무리 단계는 코드를 분석하여서 정리하기로 하였습니다. 전 플레이어 연출인 카메라 부분 및 낙하 판정을 하는 부분을 분석해보려고 합니다.

협업한 친구가 코드를 잘 정리하여 보내주었습니다.

굿

카메라가 벽에 닿을 시 처리하는 부분

카메라 값

    /*초기 값 설정*/
    private float defaultPosition;
    private float targetPosition;
    
    public Transform cameraTransform;
	
    defaultPosition = cameraTransform.localPosition.z;

    cameraTransform.localRotation = Quaternion.identity;
    cameraTransform.parent = pivotTransform;
    
    // TPS 카메라 보정
    // 카메라와 플레이어 사이에 물체가 있을 때 레이로 충돌판정, z 거리조절
    private void CameraCollision()
    {
        // 카메라 기본위치
        // 충돌이 없는경우 이 값으로 이동 될 수 있게 따로 변수 만듦
        targetPosition = defaultPosition;

        RaycastHit hit;
        // 카메라에서 카메라 pivot 으로 향하는 방향벡터
        Vector3 direction = cameraTransform.position - pivotTransform.position;
        // 정규화(방향만 필요함)
        direction.Normalize();

        // 스피어캐스트 사용, pivotTransform.position위치에서 direction 방향으로 광선을 쏘고 그 광선을 따라서
        // 반지름 0.2크기의 구를 날려서 부딪히는지 체크, 마지막 매개변수는 광선의 사거리로 카메라와 플레이어 사이 기본거리
        if (Physics.SphereCast(pivotTransform.position, 0.2f, direction, out hit, Mathf.Abs(targetPosition)))
        {
            // 플레이어와 카메라 사이 물체가 있는경우
            // distance = pivot과 충돌물체 사이의 거리값 저장
            float distance = Vector3.Distance(pivotTransform.position, hit.point);
            
            // 카메라 위치값 보정
            // 카메라z 위치 = distance - 보정값(카메라 얼마나 플레이어 쪽으로 땡길지 값)
            // - 붙은 이유 : 카메라 로컬 좌표, 플레이어 뒤에서 찍기 때문에
            targetPosition = -(distance - tps_CameraCollisonDistance);
        }

        // 위에서 보정 후에
        // 카메라와 pivot 사이거리가 보정값보다 짧은경우, 즉 너무 벽에 붙어있어서 벽과 플레이어 사이거리가 보정값 보다 짧은 경우
        if (Mathf.Abs(targetPosition) < tps_CameraCollisonDistance)
        {
            // 최소 플레이어와 카메라 사이 거리를 지정해줌
            // 최소값 따로 안만들고 보정값이 적당해서 그대로 사용함
            targetPosition = -tps_CameraCollisonDistance;
        }

        // 보정한 값
        // Lerp로 부드럽게 이동시킴
        cameraPosition.z = Mathf.Lerp(cameraTransform.localPosition.z, targetPosition, Time.deltaTime / tps_cameraOffsetSpeed);

        // 카메라 z위치 최종 보정값 실제 반영
        cameraTransform.localPosition = cameraPosition;

        // 카메라가 바라보는 기준을 pivot으로 해놓은 이유 : 플레이어에 변동사항이 생겨도 수정이 용이하게 하기위함
        // 스피어캐스트 사용이유 : 더 정밀하게 보정할 수 있기 때문에, 그냥 레이캐스트 사용시 체크하지 못하는 부분까지 체크해줌
    }

 

플레이어 낙하 판정 부분

레이캐스트를 이용한 판정 (빨간선)
값 설정

    // 플레이어 낙하 판정
    // 바닥 거리판정이기 때문에 여기서 플레이어 y위치 처리도 함
    private void PlayerFalling()
    {
        RaycastHit hit;
        // 캡슐콜라이더의 높이의 절반 길이
        float length = capsuleCollider.height * 0.5f;
        // 콜라이더 박스의 중심 점
        Vector3 origin = capsuleCollider.bounds.center;

        // 레이캐스트, 플레이어의 콜라이더 중심으로 부터 아래 방향으로 콜라이더 절반 길이 + 최소 낙하판정 거리
        // 사거리 저렇게 두고 충돌하면 바닥에 닿고 있거나 떨어져도 낙하 동작 나올만큼의 높이가 아닌 경우
        if (Physics.Raycast(origin, Vector3.down, out hit, length + minimumDistanceFalling))
        {
            // 확인용
            Debug.DrawRay(origin, Vector3.down * (length + minimumDistanceFalling), Color.red);
            
            // 이 함수 안에 들어왔으면 바닥에 닿고 잇는 상태
            inputCtrl.isGround = true;

            // hit.distance = 부딪힌 지점까지의 거리
            // 이 거리가 콜라이더 절반 길이보다 길면(= 바닥에 서 있는 모든 경우)
            if (hit.distance > length)
            {
                // 플레이어의 위치 저장, 현재 위치에서 y값만 보정해주기 위해서
                Vector3 newPos = transform.position;

                // 현재 위치의 y값을 충돌지점의 y값으로 바꾸어준다. 즉 바닥의 y좌표랑 플레이어 y좌표랑 똑같이해서 바닥아래 발이 안빠지게 함
                newPos.y = hit.point.y;
                // 변경한 위치를 실제 플레이어 위치에 반영
                transform.position = newPos;
            }

 

주말동안 알고리즘 책을 읽고 나서 문득 생각해보니 코드테스트 전 합격 여부인 포폴이 중요한데

포폴을 좀 더 정리해야겠다고 생각해서 문서를 보강하기로 했습니다.

먼저 애너미 구조 설계를 상속 형태로 리팩토링, AI를 Nav Mesh Agent로 구현, 애너미 애니메이션을 살펴보겠습니다.

리팩토링전 스크립트입니다. 보기와 같이 fleldOfView와 같이 외부 스크립트 의존성이 높고 전체적 가독성이 떨어지며 확장성, 다양성이 좋지 못합니다.

리팩토링 후 스크립트입니다. 가독성은 말할 것도 없이 좋아졌고 MeleeEnemy라는 파생 스크립트로 근거리 애너미를 만들었습니다. 다양성이 좋아졌습니다. 확장 또한 가능하게 세분화한 상태입니다.

상속을 사용 시 협업 시 유리합니다. EnemyAttack 이라는 함수를 만들지는 않았지만 상위 스크립트에 추상적으로 만들어놓기만 하여도 다른 사람이 EnemyAttack를 가져가 사용할거나 재정의가 가능합니다. 아무런 기능을 하지 않아도 말입니다. 이런면에서 전체적인 틀만 만들어놓으면 재정의 후 수정해서 기능을 다시 만들수도 있습니다.

 

추적, 정찰, 정지 Enemy AI입니다. Nav Mesh Agent로 구현하였고 아래와 같은 형식으로 구현됩니다. 

사용할 맵을 Navigation를 이용해 Bake한 상태입니다. 건물 오브젝트는 올라가지 못하게 제외하였습니다.

애너미 정찰등 구현 방법은 아래 포인트를 따라가서 가까울 시 멈추고 다음 포인트를 탐색하는 형식입니다. 결국 이동도 이와 같은 형식입니다.

이동 관련 애니메이션은 한번에 처리하였습니다.

if문으로 상태를 검사하여 SetFloat으로 값을 반환하여 블렌더 트리를 이용해 자연스러운 변경을 이끌어냈습니다.

0일땐 Idle 상태 정지 상태, 1일땐 wark 상태 정찰 상태 ,2일땐 run 상태 추적 상태입니다.

0 Idle 정지 상태
1 Walk 정찰 상태
2 Run 추적 상태

다음 애니메이션 매니저입니다. PlayerTargetAnimation 함수를 가지고 있습니다. 호출해주는 형식입니다. Off 기능이 없는 이유는 아래 애니메이션 구성을 보면 알 수 있습니다.

보시는거와 같이 끝나야하는 행동은 다시 돌아가기만 해논 상태로 호출하고 실행 후 돌아가는 형태입니다. 

Die 같은 경우는 죽고 나서 다시 돌아갈 이유가 없으므로 끊어논 상태입니다.

그리고 각 오브젝트 당 애니메이션의 행동이 다르기 때문에 상속을 받아 애니메이션이 실행 시 OnAnimatorMove가 실행되어 각종 오브젝트 별로 값을 재설정해서 애니메이션을 자연스럽게 만들었습니다.

다음 어택 애니메이션의 구현은 두가지 애니메이션을 랜덤 돌립니다. 그리고 리소스를 아끼기 위해 어택 1,2에 대한 값은 스크립터블 오브젝트에 작성해논 값을 참조만 해서 사용합니다. 스크립터블 오브젝트 파일은 에셋에만 존재함으로서 미리 배열에 넣어둔 상태입니다.

어택 1,2 애니메이션을 랜덤 구현
Attack 1, Attack 2 스크립터블 오브젝트를 넣어둔 상태

위 파일을 만드는 방법은 아래 코드에 CreateAssetMenu를 이용하여 사용 가능합니다.

스크립터블 오브젝트 구현

애너미의 주요부분을 다 살펴봤습니다.

 

전에 한번 공부해야겠다고 생각해서 작성했습니다.

TCP(Transmission Control Protocol) - 서버와 클라이언트간에 데이터를 신뢰성 있게 전달

데이터를 송신할 때 마다 확인 응답을 주고 받는 절차 (TCP 3 Way-Handshake & 4 Way-Handshake)가 있어서 통신의 신뢰성을 높인다.

3 Way-Handshake는 TCP / IP 네트워크에서 서버와 클라이언트를 연결하는데 사용되는 프로세스입니다.

접속 요청 프로세스 A가 연결 요청 메시지 전송 (SYN)

접속 요청을 받은 프로세스 B가 요청을 수락했으며, 접속 요청 프로세스인 A도 포트를 열어 달라는 메시지 전송 (SYN + ACK)

접속 요청 프로세스 A가 수락 확인을 보내 연결을 맺음 (ACK)

4 Way-HandshakeTCP/IP 네트워크 환경에서 서버와 클라이언트를 연결을 해제(세션 종료)하는데 필요한 프로세스입니다.

A에서 B와의 연결 종료를 위해 A에서 FIN 패킷을 보냄 FIN_WAIT1 상태

B는 A로부터 FIN을 받고 응답 패킷 ACK를 보냄 CLOSE_WAIT상태

연결을 종료할 준비가 되면 A에게 FIN 패킷을 보냄 LAST_WAIT

상태A는 확인 패킷 ACK를 보냄 TIME_WAIT 상태

 

 

상태 설명
FIN_WAIT1  Close 를 호출한 측의 소켓이 진입하는 상태, FIN 보냄 
CLOSE_WAIT  Close를 받으면 CLOSE_WAIT 상태로 진입함, Ack 보냄 
FIN_WAIT2  Ack 신호를 받은 소켓은  FIN_WAIT1 > FIN_WAIT2로 상태 변경됨 
LAST_WAIT  Close 호출 후 진입하는 상태, FIN 보냄 
TIME_WAIT Close를 받으면 진입하는 상태, ACK 보냄 
CLOSED   연결 종료!!!

표 출처 : [TCP] 4-way Handshake란? / 와이어샤크

단어 정리

FLAG 설명
SYN(연결 요청 플래그) - TCP에서 세션을 성립할 때 가장먼저 보내는 패킷, 시퀀스 번호를 임의적으로 설정하여 
  세션을 연결하는 데에 사용되며 초기에 시퀀스 번호를 보내게 된다.
ACK(응답플래그) - 상대방으로부터 패킷을 받았다는 걸 알려주는 패킷- 다른 플래그와 같이 출력되는 경우도 있습니다.
- 받는 사람이 보낸 사람 시퀀스 번호에 TCP 계층에서 길이 또는 데이터 양을 더한 것과 같은  ACK를 보냅니다.(일반적으로 +1 하여 보냄) - ACK 응답을 통해 보낸 패킷에 대한 성공, 실패를 판단하여 재전송 하거나 다음 패킷을 전송한다.
FIN(연결종료 플래그) - 세션 연결을 종료시킬 때 사용되며 더이상 전송할 데이터가 없음을 나타낸다.
RST(연결 재설정 플래그) - 재설정(Reset)을 하는 과정이며 양방향에서 동시에 일어나는 중단 작업이다. 
- 비 정상적인 세션 연결 끊기에 해당한다. 
- 이 패킷을 보내는 곳이 현재 접속하고 있는 곳과 즉시 연결을 끊고자 할 때 사용한다
PSH(밀어넣기) - TELNET과 같은 상호작용이 중요한 프로토콜의 경우 빠른 응답이 중요한데, 이 때 받은 데이터를 즉시 목적지인 OSI 7 Layer 의 Application 계층으로 전송하도록 하는 FLAG. - 대화형 트랙픽에 사용되는 것으로 버퍼가 채워지기를 기다리지 않고 데이터를 전달한다. - 데이터는 버퍼링 없이 바로 위 계층이 아닌 7 계층의 응용프로그램으로 바로 전달한다.
URG(긴급 데이터 플래그) - Urgent pointer 유효한 것인지를 나타낸다. - Urgent pointer란 전송하는 데이터 중에서 긴급히 전당해야 할 내용이 있을 경우에 사용한다. - 긴급한 데이터는 다른 데이터에 비해 우선순위가 높아야 한다. 

표 출처 : [TCP] 4-way Handshake란? / 와이어샤크

좀 더 자세한 설명은 Crocus

그림출처 : TCP/IP기초(Subnetting, Ethernet&IP 구조, TCP, UDP)

 

UDP(User Datagram Protocol) - 비연결형, 신뢰성 없는 전송 프로토콜

UDP는 TCP와 함께 데이터 그램으로 알려진 다문 메시지를 교환하기 위해 사용하는 프로토콜이다. 데이터만 보내고 확인 응답과 같은 절차를 생략할 수 있으므로 통신의 신속성을 높인다.

물리, 데이터링크에서는 MAC 주소로 대상 기기를 찾아내고 네트워크 계층에서는 IP 주소를 이용해 대상 인터페이스(호스트) 를  찾아냅니다. 그러면 이제 대상 기기에도 어떤 프로세스에 데이터를 전할 것인가가 필요한데, 이때 전송 계층에 해당하는 UDP 가 프로세스의 포트 번호를 가지고 있습니다. 포트번호를 이용해 프로세스의 통로를 식별해줍니다.
출처: https://programming119.tistory.com/148

주로 사용되는 서비스 : 디스코드와 같은 데이터에 대한 지연을 피해야하는 실시간 프로그램

TCP와 UDP

두 프로토콜을 구분하는 주요한 차이는 통신의 신뢰성이냐 신속성 차이입니다.

이미지 출처 : What is UDP? | Cloudflare

+ Recent posts