<서론>

더보기

분산서버를 만드려면 서버와 서버의 연결은 필수다,,

뭐 다른 소켓을 쓴다면 소켓을 기반으로 통신이 되지만 IOCP를 쓴다면 CompletionKey로 구분을 한다.. 그리고 WSAOVERLAPPED 구조체를 이용하여 op를 구분하고 gqcs에서 넘겨받은 wsaover로 이제 할 일을 하는데,,

예전에 했던 프로젝트에서는 Accept를 받고, 서버면 특정 패킷을 한 번 더 보내서 CompletionKey를 따로 구분을 했는데,클라이언트가 이 패킷을 알아서 (해킹으로 무한히 실험해보던가, 아니면 내부 코드를 아는 사람이라던가의 이유로)  시도한다면 이는 반드시 큰 문제이다. 이를 해결하기 위해 생각을 하다가 AcceptEx에서 데이터를 받을 수 있다는 얘기를 듣고, ConnectEx의 존재를 알게되어 이것을 사용해보는것이 목적이다.

 

 

헉!!!!!!!!! 방금생각난건데 굳이 connectEx를 안 쓰더라도, 처음에 가동시킬 서버의 갯수를 안다면 굳이 이렇게 할 필요가 없다,,, 나는 뻘짓을 한것인가..? ㅜㅜ

 

하지만.. 그래도 포스팅한다,,,,

 

주된 요는 Connect할때 특정 data를 받아서 서버인지 클라인지 구분한다는게 point 이다.

본론

보통 비동기로 Accept를 받을때는 AceeptEx를 사용한다.

https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nf-mswsock-acceptex 를 보면 

AcceptEx의 인자는

BOOL AcceptEx(
  [in]  SOCKET       sListenSocket,
  [in]  SOCKET       sAcceptSocket,
  [in]  PVOID        lpOutputBuffer,
  [in]  DWORD        dwReceiveDataLength,
  [in]  DWORD        dwLocalAddressLength,
  [in]  DWORD        dwRemoteAddressLength,
  [out] LPDWORD      lpdwBytesReceived,
  [in]  LPOVERLAPPED lpOverlapped
);

이다. 보통은 accept할때 데이터 주고받을게 없다면 4번째 인자를 0을 주면된다.

하지만 우리의 목적은 Connect할때 데이터를 줘서 서버/클라 구분을 할거니깐 4번째 인자를 설정하도록 한다.

얼만큼? 설정한 wsabuf 만큼! (더 작게 줘도 됨)

다만!!!!!!!!!!!!!!!!! 주의점이 있음

저 위의 acceptEx의 dwReceiveDataLength(4번째인자) 의 설명을 보면

버퍼의 시작 부분에 있는 실제 수신 데이터에 사용할 lpOutputBuffer 의 바이트 수입니다. 이 크기에는 서버의 로컬 주소 크기나 클라이언트의 원격 주소가 포함되지 않아야 합니다. 출력 버퍼에 추가됩니다. dwReceiveDataLength가 0이면 연결을 수락해도 수신 작업이 발생하지 않습니다. 대신, 데이터를 기다리지 않고 연결이 도착하는 즉시 AcceptEx 가 완료됩니다.

라고 되어있음. 우리는  빨간색을 주목하면 됨. 서버의 로컬주소크기 (IPv4기준 4 127.0.0.1 이건 4바이트. char 4개니까)랑 클라이언트 원격주소 (이것도 4바이트)가 포함이 안되어야함. 버퍼크기가 200이라면? 8을 뺀 192까지만 받을 수 있는거임. 193을 받는다고하면 바로 데이터 못읽고 깨져버림! 궁금하면 해보도록 하자

 

자 그러면 얼 추 알아냈음.

이제 ConnectEx의 설명을 보러 가자.

LPFN_CONNECTEX LpfnConnectex;

BOOL LpfnConnectex(
  [in]           SOCKET s,
  [in]           const sockaddr *name,
  [in]           int namelen,
  [in, optional] PVOID lpSendBuffer,
  [in]           DWORD dwSendDataLength,
  [out]          LPDWORD lpdwBytesSent,
  [in]           LPOVERLAPPED lpOverlapped
)
{...}

https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nc-mswsock-lpfn_connectex

 

LPFN_CONNECTEX(mswsock.h) - Win32 apps

ConnectEx 함수는 지정된 소켓에 대한 연결을 설정하고 연결이 설정되면 필요에 따라 데이터를 보냅니다.

learn.microsoft.com

에 나와있는 ConnEx 설명임.

 

얘는 좀 특이한게, 함수를 바로 쓸 수가 없고, 함수포인터로 등록을 해서 써야 함;; 그리고 이미 바인딩 된 소켓을 사용해야함!

더보기

참고ConnectEx 함수에 대한 함수 포인터는 지정된 SIO_GET_EXTENSION_FUNCTION_POINTERopcode를 사용하여 WSAIoctl 함수를 호출하여 런타임에 가져와야 합니다.WSAIoctl 함수에 전달된 입력 버퍼에는 값이 ConnectEx 확장 함수를 식별하는 GUID(Globally Unique Identifier)인 WSAID_CONNECTEX 포함되어야 합니다. 성공하면 WSAIoctl 함수에서 반환된 출력에 ConnectEx 함수에 대한 포인터가 포함됩니다. WSAID_CONNECTEX GUID는 Mswsock.h 헤더 파일에 정의되어 있습니다.

SOCKADDR_IN server_addr;
ZeroMemory(&server_addr, sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

mSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (bind(mSocket, reinterpret_cast<sockaddr*>(&server_addr),
	sizeof(server_addr)) == SOCKET_ERROR)
{
	std::cout << "bind error\n";
	return;
}

LPFN_CONNECTEX Conn;
GUID guid = WSAID_CONNECTEX;
DWORD dwbyte{};

WSAIoctl(mSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
	&guid, sizeof(guid),
	&Conn, sizeof(Conn),
	&dwbyte, NULL, NULL);

등록하는 방법은 이렇게 됨.

주의사항을 보면 conn할때 데이터를 보내는게 되기는 한다만, 데이터 다 보낼때까지 시스템 메모리를 많이 쓰니까 많은 양을 보내지는 말라고 함. 조금만 보내도록 하자.

자 ! 그럼 이제 데이터를 보내볼 차례이다. 

 

보내는 데이터는 WSABUF로 보내므로 

WSABUF temp;
unsigned char tbuf[120];
memcpy(tbuf, &데이터쪼가리, sizeof(데이터쪼가리));
temp.buf = (char*)tbuf;
temp.len = sizeof(tbuf);

를 통해 만들어주도록 하자. 

int ret = Conn(mSocket, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr),
	tbuf, sizeof(tbuf),
	NULL, reinterpret_cast<LPOVERLAPPED>(overex));

그리고 보내주면 끝~~~

 

비동기이기 때문에 그냥 넘어간다.

 

 

 

하지만 궁금할 수 있다. (완료된걸 어떻게 알지 ?)

 

필자는 IOCP를 사용하고 있다. 그리고 ConnectEx또한 비동기함수 이고 wsaoverlapped를 쓰기 때문에 iocp로 완료처리를 받을 수 있다.

 

How to?

socket을 iocp에 등록해주고 확장 overlapped  구조체로 op만 설정해주면된다!

 

	CreateIoCompletionPort(reinterpret_cast<HANDLE>(mSocket), gMainServer->GetIOCPHandle(), 0, 0);
	WSA_OVER_EX* overex = new WSA_OVER_EX();
	overex->SetCmd(eCOMMAND_IOCP::CMD_SERVER_CONN);

wsa_over_ex 구조체는 필자가 만든것이기 때문에 여러분이 직접 만드시길 바란다. 이 글을 본다면 이정도는 할 줄 알거라 생각한다

참고로 overex를 new 할당을 안받고 만들어도 되긴 한다만,, 스택에 만든 지역변순데 gqcs에서 온전히 가져온다!. 이건 꽤 찜찜한 이야기다. new로 받고 gqcs에서 delete로 해제하도록 하자 ㅡㅡ;;

 

그래서 연결이 성공하면 gqcs에서 반환을 할거고 여기서 이제 우리는 서버를 등록하고 wsarecv를 걸어두면 된다.

BOOL ret = GetQueuedCompletionStatus(mMainServer->GetIOCPHandle(), &bytes, (PULONG_PTR)&iocp_key, &overlapped, INFINITE);
WSA_OVER_EX* wsa_ex = reinterpret_cast<WSA_OVER_EX*>(overlapped);
...
case eCOMMAND_IOCP::CMD_SERVER_CONN:
{
	auto lobbyserver = mMainServer->GetLobbyServer();
	lobbyserver->AcceptSetting(eSocketState::ST_ACCEPT, eCOMMAND_IOCP::CMD_SERVER_RECV, lobbyserver->GetSocket());
	lobbyserver->PreRecvPacket(NULL, 0);
	lobbyserver->RecvPacket();
	delete wsa_ex;
	std::cout << "Lobby Connect Complete\n";
	break;
}

 

코드 전문

	SOCKADDR_IN server_addr;
	ZeroMemory(&server_addr, sizeof(server_addr));
	server_addr.sin_family = PF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	mSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (bind(mSocket, reinterpret_cast<sockaddr*>(&server_addr),
		sizeof(server_addr)) == SOCKET_ERROR)
	{
		std::cout << "bind error\n";
		return;
	}

	CreateIoCompletionPort(reinterpret_cast<HANDLE>(mSocket), gMainServer->GetIOCPHandle(), 0, 0);
	WSA_OVER_EX* overex = new WSA_OVER_EX();
	overex->SetCmd(eCOMMAND_IOCP::CMD_SERVER_CONN);
	server_addr.sin_port = htons(LOBBYSERVERPORT);
	inet_pton(AF_INET, LOOBYSERVER_ADDR, &server_addr.sin_addr);

	LPFN_CONNECTEX Conn;
	GUID guid = WSAID_CONNECTEX;
	DWORD dwbyte{};

	WSAIoctl(mSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
		&guid, sizeof(guid),
		&Conn, sizeof(Conn),
		&dwbyte, NULL, NULL);

	WSABUF temp;
	unsigned char tbuf[120];
	memcpy(tbuf, &INCODE_SERVER_PACKET, sizeof(unsigned long long));
	temp.buf = (char*)tbuf;
	temp.len = sizeof(tbuf);

	int ret = Conn(mSocket, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr),
		tbuf, sizeof(tbuf),
		NULL, reinterpret_cast<LPOVERLAPPED>(overex));
	if(ret == SOCKET_ERROR)
	{
		int err = WSAGetLastError();
		if (err != WSA_IO_PENDING)
		{
			std::cout << "error\n";
			return;
			//error ! 
		}
	}

 

자 이렇게 하여 성공적으로 connect시 데이터를 보냈다.

 

놀랍게도 아직 끝이 아니다

받는쪽은 어떻게 받냐? 도 중요한 부분이다.

 

AcceptEx(mListenSocket, c_socket, a_over.mMessageBuf, 
	BUF_SIZE - 8, 
    addr_size + 16, 
    addr_size + 16, 0, &a_over.mOver);

맨 위쪽에서 설명한걸 코드로 보여주면 대충 이렇다.

 

받는쪽에서는 그대~로 받아주면 된다. AcceptEx에 등록했던 저 버퍼를 GQCS에서 그대로 꺼내주기만 하면 끝~

	unsigned long long k{};
	memcpy(&k, exOver->mMessageBuf, sizeof(unsigned long long));

필자는 long long 값 하나를 보냈으므로 받을때도 똑같이 받는다.

 

다만 문제가 있다. acceptEX에서 100바이트를 받을게요~ 라고 설정했을때, 

Connect해주는애가 아무것도 안보내면 무언가 데이터를 받을 때까지 gqcs가 반환을 안한다;;

 

이렇게 된다면,, 클라이언트에서도 ConnectEx로 데이터를 보내는게 아니라면, 어차피 이 gqcs반환을 위해서 특정 번째까지 또 처리를 해줘야 한다는 불편한 점이다..

 

이럴거면 그냥 connectEX 안쓰고 connect로 하고 특정 번째까지만 서버라고 판정하면 되는거 아님?? 이라고 할 수 있다.

 

맞는말이다. 근데 해보기 전까지 몰랐잖아!!!!!!!!!!!!!

난 해봤으니까 아는거다.. 이런 삽질을 안해도 된다는 것을

근데 의미가 있을 수 있다. 클라이언트에서도 ConnectEx로 연결을 한다면, 이는 정말 유의미한 방법이다.

클라에서는 1byte만 connectEx에 담아 보내고, 서버는 ConnectEx에 10바이트를 담아 보내면 그냥 바로 구분할 수 있다.

 

1. 서버 런타임 도중에 서버및 클라 연결을 시도해야하고, 이때 구분자가 필요하다면 이 방법이 유효할 수 있다. (서버 재가동시 구분자용)

2. 하지만 우리 서버는 클라접속 전에 이미 서버세팅을 다 끝내놓기 때문에 딱히? 의미가 없을 수 있다.

 

나중에 1번방법이 필요하다면 이 방법을 사용해서 연결하도록 하자.

'개발 > C++' 카테고리의 다른 글

lua coroutine을 이용한 퀘스트 스크립트  (1) 2023.07.12
첫 글  (0) 2022.08.12

<서론>

더보기

기본적으로 나는 ue4가 지원하는 데디서버를 사용하지 않고, 자체 제작 서버를 사용함.

이유는 데디서버를 사용하면 언리얼에서밖에 사용 못하는 제한성도 있고, 아랫단을 직접 다뤄보지 않고 그저 '딸깍' 하나로 모든게 다 되는 걸 경험하면 공부에도 딱히 도움이 될 것 같진 않아서,,, 임.

그 외에도 DB같은 경우는 어차피 따로 연결을 해야 하는 부분이고,, 엔진의 제약없이 내 맘대로 짤 수 있다는 메리트 때문에 선택을 함.

<본론>

자체서버를 사용할 시, 이동 동기화를 하는 것,, 나같은 경우는 어떻게 했나?

를 설명하기 전에 근본적인 문제점 부터 말하자면

자체제작 서버이기 때문에, 언리얼 내의 물리엔진을 가져오지 않는 이상, 서버에서 이동로직을 돌리는 건 좀 힘들다..

이거 때문에 데디랑 자체랑 고민을 좀 많이하긴 했었긴 함. 

언리얼 내부 물리엔진을 포기하고 서버단에서 따로 물리엔진 처리를 한다..? 작업량이 뻥튀기 되는데 이럼 언리얼로 하는 메리트가 없어진다고 해야할까... 하여간 복합적인 이유로 이동 자체는 클라에서 처리하기로 했다.

 

<접근법>

기본적인 접근은 클라이언트에서 움직인 것을ex)좌표값 etc. 서버로 보내고 서버는 다른 클라에게 그것을 보내줌.

문제점 : 매 프레임마다 보내나? 그럼 클라 사양에 따라서 패킷을 많이 보내는 컴터가 있고, 적게 보내는 컴터가 있겠네?

-> 이것때문에 패킷을 보내는 속도는 30프레임으로 고정함.

 

패킷을 30프레임으로 고정했기 때문에, 기본적으로 60fps 이상으로 돌아가는 눈에는 30fps와 60fps만 해도 뭔가 뚝뚝 끊기고 어색함이 보임. 이것을 보간 해주어야 함.

 

<패킷으로 무엇을 보내야 하나?>

1. 먼저 ue4에서의 걷는 animation은 anim locomotion에서 speed에 따라 보간이 됨. 따라서 이 값을 동기화 해주지 않으면 move anim이 끊기게 됨.

 

2.캐릭터의 위치 값과, 회전값도 일단 동기화를 해야 함.

 

3. 언리얼 내에서 velocity로 캐릭터가 움직이기 때문에 이 값도 동기화를 해야 함.

 

그래서 이동패킷 구조는 아래와 같음

이동패킷

 

그런데 설명하지 않은 값, inair 가 있음 이건 점프 애니메이션 때문에 따로 추가한 것임.

 

<문제점>

이렇게 딱 보내고 되면 좋겠지만,,, 점프 할 때 또는 다른 때에 간헐적으로 드드드드드 떨리는 현상이 좀 일어남.

꽤 장기간에 걸쳐 보완의 보완을 거쳐서 잡아냈음.

 

1. velocity로 속도 보간을 했더니 특히 더 심해졌었음. 이유는 엔진 자체에서도 물리엔진이 돌아가기 때문에 (Gravity, friction)이 값이 알아서 계산이 되어서 덜덜덜 떨리는거였음. 그래서 내가 컨트롤 하는 character가 아니라면 Braking Deceleration walking과 friction factor를 off시켜서 바닥에서의 위치오차를, Gravity를 off시켜서 공중에서의 위치오차를 없앰.

off시키는 방법

더보기
character의 beginplay() 안의 짤막한 함수이다. 이렇게 하면 공중에서 안떨어지고 멈춰있게 됨. 바닥에서 미끄러지지도 않고

 

2. gravity를 꺼서 공중오차를 없앤 것 같지만, 아직 오차가 있음 아주 미세한 위치차이 (내 예상에는 부동소수점 오류 같음. 확실하진 않음...)그래서 A클라에서는 땅에 닿았지만 B에서는 아주 미세하게 공중에 떠있는 판정이 되어서 jump 애니메이션이 실행되는 경우가 있었음. 아 영상을 따놓을걸!!!!!!!! 정말 보면 속상함 

ue4 editor상의 anim graph

더보기
내 캐릭터의 애님 그래프

idle 상태에서 jump로 가려면 in air 값이 true/false에 따라 결정되는데, 그래서 in air값을 move 패킷에 실어서 보내준 것 임.

 

3. 참고로 이 in air값은 BP에서 설정되고 있던 값이라, cpp에서 다루려면 Anim Locomotion BP 관련을 cpp로 모조리 바꾸거나, Delegate를 이용하여 cpp to BP를 해야함. 이 로코모션 같은 경우는 동료들이 짜놓은거라 나는 후자를 택함. 웬만하면 모두 C++로 짜고 싶었지만.. 이것 때문에 BP 사용이 불가피했었음 ㅠㅠ...

델리게이트 적용 사진

더보기

cpp에서 델리게이트 등록하고 bp에서 적용시킨거임.

 

4. in air를 동기화 했으니 끝~ 인 줄 알았더니!!! 사실 이 in air라는 값은 클라이언트 엔진 내부에서 자체 계산되고 있던 is falling을 통해서 재 설정되고 그렇기에 문제는 여전히 해결되지 않았음 ㅠㅠ.... (is falling 자체도 동기화 해봤지만 문제는 같았음) -> 그래서 자체적으로 계산되고 있는 inair 변수 말고 동기화를 위한 새로운 변수 inAir(Network)를 하나 선언하여 이걸로 동기화를 함.

 

5. in air(Network)가 생겼기에 기존 InAir로 돌아가던 Anim Graph를 수정해야 했음. 먼저 동기화 될 캐릭터는 컨트롤이 되지 않는 캐릭터들 이므로 이것을 체크하는 bool 값 isPossess를 하나 선언함

그리고 연산을 통해 기존의 값을 해치지 않고 값을 도출 함 (IsPossess && InAir) || InAir(Network) 이렇게 하여 컨트롤 되지 않는 캐릭터들은 자체적으로 동기화 해준 in air값으로 강제로 공중에 떠있거나, 땅에 붙을 수 있게 됐음 !

 BP에서의 모습

 

6. 그리고 속도와 위치만으로 동기화를 할 경우, 이동패킷이 30fps로 동기화되는 것 때문에 점프 반응속도가 느려짐. 그래서 점프 키를 누르면 이것에 대한 동기화를 바로 해줌.

 

 

여기까지 했다면 버그가 사라짐! 

 

<결론>

꽤 오랜 시간에 걸쳐 수정 수정 수정 된 내용임... (콘텐츠 만들면서도 계속 이동에 관한 문제는 생각을 하고 있었음)

생각보다 엔진 자체적으로 계산되고 있던 값 때문에 일어난 내용이 많았고,,, 서버 담당이었지만 뭔가 엔진 공부만 주구장창 했던 기억이 ^^..... 참 웃프다 그래도 덕분에 언렬 엔진을 좀 더 알게 되어서 지식은 늘었다 정도? ㅎㅎ...

'개발 > Unreal Engine 4' 카테고리의 다른 글

나무 찾기 알고리즘  (0) 2022.08.12

<서론>

DB쪽을 공부하기 위해 뭐 좋은게 없을까 하다 시작한 퀘스트 컨텐츠 넣기

개발을 시작하려고 설계를 잡다보니 예전에 실습했던 곳에서 스크립트로 퀘스트를 짰던 기억이 나서 그것과 유사하게 짜보려고 시도

당연하게도 실습학생이었기 때문에 C++코드는 보지도 못했고, 단순하게 스크립트 코드만 보고 어떻게 동작하는지 예상해서  코드를 짜야 했다.

 

그때 당시에 스크립트에서 아이템을 주는 코드도 있었기에 처음엔 서버에서 처리해야 된다고 생각했음.

하지만 이내 문제에 봉착함.
dialog는 예/아니오 분기로 나뉘는데 서버에서 클라의 key input을 기다려야 하는게 말이 되나?

머리가 어지러웠음. 분명 클라의 예/아니오 누른것에 따라 분기가 나뉘어야하는데 클라의 행동을 예측할 것도 아니고 어떻게 기다리지?

-> 1주일동안 생각해도 딱히 떠오르는게 없어서 그냥 클라이언트에 스크립트를 넣기로 함.

 

이후 주변사람과 얘기하던 도중 '그래도 서버에 넣는게 맞지않아? 도망치지마!!' 라는 얘기를 듣고 그래 한 번 해보자 하고 일단 부딪혀보기로 함. (사실 몇가지 더 찾아보고 결정을 내린거지만,, 생략)

 

<접근법>

먼저 이것을 해결하기 위해서는 적어도 비동기적인 방식이 필요하다고 생각했음. 서버는 무작정 클라이언트의 input을 기다리지 않고, 일단 할 일을 하다가 요청이 들어오면 다시 진행되는 식으로

ㅡ> 멀티스레드 방식을 생각했음 WaitForSingleObject()같은거

lua에서 멀티스레드가 가능한가? 를 검색하다가 coroutine이 있다는걸 확인하게 됨.

 

이거다

 

<루아의 코루틴>

루아에서는 일단 루아 자체적으로도 coroutine이 있고, C API로도 제공을 함. 

하지만 C API는 설명문서를 보면

https://wariua.github.io/lua-manual/5.4/manual.html#lua_yield

 

루아 5.4 참조 매뉴얼

 

wariua.github.io

무슨 얘긴지를 잘 모르겠음 예제가 없기 때문에 검색하거나 직접 부딪히면서 알아가야하는데

직접 써보니까 더욱 난해함 일단 인자로 넣어야 할 값들이 애매하다 코루틴이 실행을 중지한다라는 의미가 C API로 멈춘다는건지 lua에서 coroutine.yield로 멈춘다는건지 명시적으로 설명된것도 없어서 쓰면서 너무 애를 먹었음.

 

반면 lua의 coroutine테이블에 있는 함수들로도 코루틴이 구현 가능함.

이건 꽤나 단순하고 사용이 쉬움. 테스트코드를 짜는데도 문제가없고 인자들도 간단명료함. 그래서 이걸 사용하기로 함

 

<사용법>

1. 먼저 코루틴으로 동작할 함수 하나를 정의함 

function foo(value)
    print('yield start!', value)
    ret = coroutine.yield()
    print('yield end!', ret)
end

인자를 받고, yield로도 ret 값받고, 결과를 출력해주는 복합적 예시 함수다.

 

2. 코루틴을 만들어주고 시작해줌.

function main()
    value = 1
    cohandle = coroutine.create(foo)
    coroutine.resume(cohandle,value)
end

coroutine.create를 하여 새롭게 핸들을 하나 만듬(스레드타입이라고 하더라) 하지만 시작은 아님

coroutine.resume을 해야 비로소 코루틴이 시작이 됨. 이친구가 재개 및 시작을 담당함

인자로는 핸들값과 그 뒤로는 함수에 전달할 매개변수들을 쭉 넣으면 됨. 간단쓰

 

3. foo에서 그럼 yield를 하여 나갈텐데 어떻게 돌아오느냐?

function foo2()
    ret = 3
    coroutine.resume(cohandle,ret)

이렇게 하면 돌아간다.

여기서 눈여겨 봐야할건 resume의 인자인데

이 coroutine.resume은 두가지 역할을 함. cohandle위치인 핸들넣는값은 같지만 그 뒷 인자가 용도에 따라 다르다

 

1. coroutine을 시작시키는 작업 

  • coroutine함수 foo()의 매개변수로 전달할 인자 값이다. 

2. coroutine을 재개하는 작업

  • coroutine.yield가 return 해줄 값이다.

실행화면

yield start!		1
yield end!		3

 

간단하게 coroutine을 사용하는 법을 알아보았다.

 

 


진짜는 이제부터이다.

단순하게 위 처럼 짜고 서버클라 테스트를 돌려보면 곧바로 문제점에 봉착한다.

1. 플레이어 1이 npc에게 요청을 함  -( o )

2. 플레이어 2가 npc에게 요청을 함. -( o )

 

3. 플레이어 1이 npc에게 응답한다 - ( o )

4. 플레이어 2가 npc에게 응답한다 - ( x )

 

3단계에서 사실 플레이어 1은 1의 coroutine에 들어간게아니라 2가 새롭게 만든 coroutine을 사용하고 있다.

이는 cohandle를 전역변수로 만들고 그걸 모두가 같이 쓰기 때문에 일어나는 일이다.

 

이걸 해결하려면 내가 생각했던 건 

  1. 플레이어마다  lua_state*를 하나씩 다 부여를 하고 npc를 돌리던가,,
  2. 내부적으로 cohandle을 플레이어마다 하나씩 부여 하던가.

1번은 뭔가 코드가 굉장히 더러워질것 같고 좀 이상해서 패스

2번으로 하기로 했다.. 하지만 어떻게?

첫 번째 방식 - 핸들을 lua - c++ 왔다갔다 하기 

더보기

 

local value로 handle을 받고 이 값을 C++로 옮긴다음 다시 C++에서 lua로 옮기는 방식을 생각했었다.

 

놀랍게도 lua에는 기능이 있다. 

lua_tothread를 사용하면 C++에서 cohandle을 뽑아낼수 있다. (핸들은 스레드타입이라 했으므로)

 

이제 이 핸들을 lua로 다시 옮기기만 하면 된다.

lua_pushthread를 사용하여!

하지만 대차게 실패

왜인지는 정확히 모르겠으나 main thread는 옮길 수 있으나 코루틴스레드같은 따로 만든거는 push로 넘길경우 루아가 터져버린다!!!!!!

감히 예상해보건데 스레드끼리 지지고 볶고 하는걸 막는게 아닐까 싶기도 하다. 왜인지는 모름

 

두 번째 방식 - bit 뭉탱이로 보내기

더보기

그럼 단순하게 bit로 보내고 읽는 형식은 안되나? C++의 reinterpret_cast 처럼,,,

-> lightuserdata 라는게 있는걸 확인했다. 이건 void* 형식으로 데이터를 받는데 이걸통해 bit뭉탱이로 핸들을 보내고 lua에서 다시 읽는걸 생각했다.

 

그러나 type이 thread가 아니고 userdata로 되어서 실패

 

세 번째 방식 - metatable 사용하기

더보기

metatable을 사용할 순 없나? 

결론부터 말하면 안될것 같더라 

 

결국 table에 값을 넣으려면 push~ 로 스택에 올리고 공유하는데 첫 번째 방식에서 pushthread가 안되는걸 이미 알았는데 굳이 삽질하기 싫어서 시도 안했다.

 

네 번째 방식 - 단순 무식하게 때려넣기

더보기

핸들을 요리하는게 불가능하다면 그냥 lua 전역에 보관해놓는건 ?

table은 c++의 map처럼 key - value 쌍이 가능하니까 플레이어 이름으로 key를 만들고 핸들을 value로 보관이 가능하지 않을까?

 

사실 도중에 든 생각이긴 하지만 게임 특성상 많은 요청이 들어오게되면 lua 전역에 미친듯이  쌓여서 뭔가 side effect가 생기지 않을까 싶어서 시도하지 않았는데

다시 생각해보니 npc와 대화만 하면 다시 table에서 빼줄건데, 이 값이 부하가 되려면 이미 사람이 너~~~무 많이 한 npc에 몰려있다는 거고 이러면 lua가 터지기 전에 다른데서 요청이 많아 부하가 걸리지 여기서는 문제가 생기지 않을거라는 나의 생각과... 함께 타협하기로 한다. 

 

시도했고, 성공했다.

 

코드의 본문은 이런식이다.

npc0001.lua

user = {};

function foas(player)
    ret = API_NoticeWindowOK(myid,player,"Test Conversation");
    ret = coroutine.yield()

    if(ret == 1) then
        API_NoticeWindow(ret,player, "You Push Yes Button");
    elseif(ret == 0) then
        API_NoticeWindow(ret,player, "You Push No button");
    else 
        API_NoticeWindow(ret,player, "err ");
    end
end

function event_interaction_0001_NPC(player)   
   user[player] = coroutine.create(foas);
   coroutine.resume(user[player],player);
end

function foas2(player, ret)
  coroutine.resume(user[player],ret);    
end

쓸 데 없는 코드는 다 지우고, 핵심만 남겨놓았다.

 

끝마침은 ,,, 

코루틴을 이론만 듣기만 했었지 직접 써본적은 처음이었는데 뭐,,, 나름 강력한 것 같다. 기존의 코루틴은 싱글 스레드에서 멀티스레드처럼 돌아가게끔 하는 거라고 알고있는데 이게 코루틴이 맞나 싶기도 하고... ㅋㅋ

 

앞으로 저 함수들을 좀 모듈화 하고.. 여러가지 작업을 거쳐서 찍어낼 수 있게끔 할 예정이다. 

 

또 보자!

'개발 > C++' 카테고리의 다른 글

ConnectEx를 적절하게 써보자  (1) 2023.10.15
첫 글  (0) 2022.08.12

기존의 stored procedure가 실행이 잘 안되어서 일반적인 쿼리문으로 짜려고 했는데 

 

안되던 이유를 찾음.

 

https://dev.mysql.com/doc/c-api/5.7/en/c-api-multiple-queries.html

 

MySQL :: MySQL 5.7 C API Developer Guide :: 3.6.2 Multiple Statement Execution Support

3.6.2 Multiple Statement Execution Support By default, mysql_real_query() and mysql_query() interpret their statement string argument as a single statement to be executed, and you process the result according to whether the statement produces a result set

dev.mysql.com

 

내가 이해한 내용으로 적자면 

 

저장프로시저는 결과집합을 생성할 수 있으므로, 한 번 결과를 캐는게 끝났다면 

 

mysql_next_result()를 호출하여 (prepare statement에서는 mysql_stmt_next_result())

결과집합이 있는지 확인해야 한다는 것이었다.

 

그리고 이 함수를 사용하려면 mysql_connect 할 때 마지막인자에 CLIENT_MULTI_STATEMENTS 를 넣어 다중결과처리를 활성화 해야한다는 얘기 또한 있다,,

 

그동안 저장프로시저를 사용해서 select문을 실행하면 계속 syntax error가 떴던 것,,,,

prepare statement에도 똑같이 에러가 떳었는데 이것 또한 이때문이었던 것 같다 '-'....

 

고쳐서 코딩 결과 제대로 실행되는 것을 확인. 

 

다만 mysql_next_result()를 써서 결과집합이 또 있는지 체크해야하는거 때문에 코드가 길어지는 건 어쩔 수 없는듯 하다. 

또한 이것에 저장프로시저의 OUT 인자도 사용법이 애매했었는데 

 

https://dev.mysql.com/doc/c-api/5.7/en/c-api-prepared-call-statements.html

 

여기 설명이 친절하게 나와있다.. 예시랑 ^^...

검색을 잘 해보도록 하자 ! 

'개발 > MySQL' 카테고리의 다른 글

Mysql C++에서 Stored Procedure OUT 인자 사용하기.  (0) 2023.03.26

개요

C API를 이용한 MySQL에서 Prepared Statment( pstmt ) 를 사용하여 Stored Procedure(이하 SP)를 호출하려함.

 

insert나 update부분쪽은 잘 되는것을 확인 하지만 SELECT에서 왜인지 모르게 

특히 fetch관련 함수를 사용하면 핸들이 정상종료가 안되고 뒤 쿼리들도 망가지는 현상을 해결하지 못하여 

다른 방법인 stored procedure에서 OUT 인자를 사용하여 전달하려했으나 이것도 좀 난항을 겪음 

 

결국 pstmt를 사용하지 않고 일반 쿼리를 날렸으나 이것도 단순하게는 안되고 특정 방법을 사용해야 되기에 저장용으로 기술

 

CREATE DEFINER=`root`@`localhost` PROCEDURE `dummytest`(
in param1 varchar(20),
in param2 varchar(20),
out param3 smallint
)
BEGIN

select count(*) into param3 from player_data where player_name = param1 and player_password = param2;

END

 

mysql의 sp는 이정도. 간단한 쿼리이다.

이것을 호출하려면 sql 쿼리로는 call dummytest('id','pw',@param); 이렇게 된다.

 

내가 간과했던건 c++에서의 코드 작성 방법이다.

string id{ "test1" }, pw{ "1234" };
short is_ok{};
sprintf_s(query, 1024, "call dummytest('%s','%s',%d)", id.c_str(), pw.c_str(),is_ok);

지금보니 상당히 엉뚱한 코드인데, pstmt를 사용하면서

쿼리에 변수 binding을 했었어서 헷갈렸었던 것 같다.

 

정상적인 코드는 이렇다.

	string id{ "test1" }, pw{ "1234" };
	sprintf_s(query, 1024, "call dummytest('%s','%s',@param3)", id.c_str(), pw.c_str());

sql에 쿼리 날리는것과 동일하게 날려야한다는 점. @도 빼먹으면 안된다.

그러면 param3는 어떻게 얻나?

	sprintf_s(query, 1024, "select @param3");

단순하게 @param3을 얻는 쿼리를 한 번 더 치면 된다...... 이상 끝

 

코드 전문

MYSQL* hmysql;
	MYSQL* conn_result;
	unsigned int timeout_sec = 1;

	hmysql = mysql_init(NULL);
	mysql_options(hmysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout_sec);
	conn_result = mysql_real_connect(hmysql, "localhost", "root", "dltnals", "simplemmo", 3306, NULL, 0);

	if (NULL == conn_result)
	{
		cout << "DB Connection Fail" << endl;
	}
	else
	{
		cout << "DB Connection Success" << endl;

		char query[1024];
		MYSQL_RES* result{};
		MYSQL_ROW row;
		string id{ "test1" }, pw{ "1234" };
		sprintf_s(query, 1024, "call dummytest('%s','%s',@param3)", id.c_str(), pw.c_str());
		
        //Send Query
		if (mysql_query(hmysql, query))
		{
			cout << "SELECT Fail" << endl;
			fprintf(stderr, "Error %d\n%s", mysql_errno(hmysql), mysql_error(hmysql));
			return;
		}
        
		mysql_free_result(result);


		sprintf_s(query, 1024, "select @param3");
		if (mysql_query(hmysql, query))
		{
			cout << "SELECT Fail" << endl;
			fprintf(stderr, "Error %d\n%s", mysql_errno(hmysql), mysql_error(hmysql));
			return;
		}

		result = mysql_store_result(hmysql);

		MYSQL_FIELD* field;
		field = mysql_fetch_fields(result);

		int fields = mysql_num_fields(result);    // 필드 갯수 구함
		while (row = mysql_fetch_row(result))     // 모든 레코드 탐색 실패시 NULL반환 while 탈출
		{
			for (int i = 0; i < fields; i++)    // 각각의 레코드에서 모든 필드 값 출력
			{
				cout << field[i].name<<":" << row[i] << "   \n";
			}
			cout << endl;
		}

		mysql_free_result(result);
		mysql_close(hmysql);
	}

 

'개발 > MySQL' 카테고리의 다른 글

mysql에서 stored procedure를 사용하는 방법.  (0) 2023.04.01

https://www.acmicpc.net/problem/11053

 

11053번: 가장 긴 증가하는 부분 수열

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이

www.acmicpc.net

 

 

 

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cstring>
#include <queue>

using namespace std;

int memo[1001];
vector<int> per;

int dp(int idx)
{
	
	//vector 끝에 다다르면 리턴
	if (per.begin() + idx == per.end()) return 0;


	//memoization 할 값이 있으면 리턴
	if (memo[idx] > -1) return memo[idx];

	//방문
	memo[idx] = 0;

	//다음거부터 끝까지 순회
	for (auto i = per.begin() + idx; i+1 != per.end(); ++i)
	{
		//다음게 나보다 높다면
		if (per[idx] < *(i + 1))
		{
			//그다음걸 찾으러 감 
			//이때 재귀하면서 끝에서부터 오면서 length를 늘려나가는것임.
			//끝에서부터 최적의 해가 만들어지면서 오므로, 결국 앞쪽 어딘가에는
			// 최대값이 들어있을 수 밖에 없음 .
			memo[idx] = max(memo[idx], dp(i - per.begin() + 1) + 1);
		}
	}


	return memo[idx];


}
int main()
{
	int N;
	cin >> N;
	for (int i = 0; i < N; ++i)
	{
		int sa = 0;
		cin >> sa;
		per.push_back(sa);
	}
	memset(memo, -1, sizeof(memo));
	//문제는 여기임
	//dp로 최적해를 찾았다고 했는데
	//50 10 20 30 같은경우는 0번째부터시작하면 뒤는 그냥 무시하게 됨
	//모두를 다 탐색해야 하므로 다음것도 일단 넣어봄
	//어차피 최적해가 구해진 memoization이 있기 때문에 부담없음 
	for (int i = 0; i < per.size(); ++i)
	{
		dp(i);
	}
	int result = 0;
	for (int i = 0; i < 1001; ++i)
	{
		if (result < memo[i])
			result = memo[i];
	}
	cout << result + 1<< endl;

}

dp를 이렇게 해도 되려나... 하고 했는데 된 케이스

 

memoization은 그냥 신이다

AI가 원하는 나무(Object)를 찾아서 이동해야 한다 !

 

나무의 위치를 GetActorLocation으로 받아서 SimpleMoveToLocation의 인자로 받아준다면 쉽게 될 일이지만

 

이럴 경우 동적으로 움직이는 것 들이 [ex)플레이어] 끼어든다면 NavMesh가 동적으로 수정이 안돼서 AI가 플레이어에게 막혀 가만히 멈춰 서버리게되는,,, 현상이 발생한다.

 

그래서 약간의 꼼수를 곁들인 나무찾기 알고리즘을 개발

 

먼저, 기존의 FindTree노드와 GototheTree 노드로 분리돼있던걸 하나로 합쳐서 PathFindingForFarm이라는 TaskNode를 만들었다.(.cpp/.h)

 

대략적인 알고리즘 설명은 

- 나무를 찾고, 나무를 향해 가는 도중, 장애물이 끼어들으면 다른 우회선로를 선택하는 알고리즘 -

이 되겠다. 

 

먼저 OverlapMultiByChannel 함수를 통해 반경감지 내의 오브젝트를 싹싹 긁어 모아버린다.

 

필요한 것만 Channel에 넣어서 최적화 하는 방법도 있지만~ 본인은 ECC_WorldStatic 으로 긁어모았음.

 

쓸 데 없는 것도 모아져 있을테니 분리를 한번 쓱~ 해준다.

 

이때 본인은 vector에 분리된 오브젝트를 넣어주었다.

 

오브젝트의 클래스는 요렇게 된다.

class TreeInfo {
public:
	class ATree* mTree;
	bool bIgnored;	//캐릭터한테 막혀있을 경우 ignored가 켜져서 탐색에 사용되지 않을 예정
	TreeInfo();
	TreeInfo(class ATree* tree);
};

그다음 예외처리 한번 해주고~

// --- 가장 가까운 나무 찾기,
	if (trees.size() <= 0)
		return EBTNodeResult::Failed;

	//Trees에 있는 객체가 아닌 Tree의 index를 꺼내주는건, 탐색에 실패했을 때, trees의 ignore값을 true로 바꿔주고
	//재 탐색해야하기 때문.
	//재사용을 위한 코드
   TargetTreeNum = -1;
   fTreeDistance = TNumericLimits<float>::Max();
	FVector mAILocation = mAI->GetActorLocation();

	for (int i = 0; i < trees.size(); ++i)
	{
		float distance = FVector::DistSquared(mAILocation, trees[i].mTree->GetActorLocation());
		//Ignored 상태인 tree는 탐색에 넣지 않는다.
		if (trees[i].bIgnored) continue;
		if (trees[i].mTree->CanHarvest)
		{
			if (distance < fTreeDistance)
			{
				fTreeDistance = distance;
				TargetTreeNum = i;
			}
		}
	}

trees라는 vector에서 하나씩 꺼내 distance를 측정하고 제~일 가까운친구를 TargetTreeNum에다가 기록해준다.

bIgnored가 있는 이유는 이따가 설명해주겠다.

//-- 내 자신에서부터 나무로부터 광선을 쏴서 이 사이에 캐릭터가 있는지 확인한다.
	TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes; // 히트 가능한 오브젝트 유형들.
	TArray<AActor*> IgnoreActors; // 무시할 액터들.
	FHitResult HitResult; // 히트 결과 값 받을 변수.

	//TEnumAsByte<EObjectTypeQuery> WorldStatic = UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_WorldStatic);
	TEnumAsByte<EObjectTypeQuery> WorldPawn = UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_Pawn);
	//ObjectTypes.Add(WorldStatic);
	ObjectTypes.Add(WorldPawn);
	IgnoreActors.Add(ControllingPawn);

	while (1) {
		UKismetSystemLibrary::LineTraceSingleForObjects(
			mAI->GetWorld()
			, mAILocation, trees[TargetTreeNum].mTree->GetActorLocation()
			, ObjectTypes, false, IgnoreActors, EDrawDebugTrace::ForOneFrame, HitResult, true);

		ABaseCharacter* BotherPlayer = Cast<ABaseCharacter>(HitResult.GetActor());
		//------------------------------
		if (nullptr != BotherPlayer)
		{
			//나무와 나 사이에 적이 있다는 얘기
			trees[TargetTreeNum].bIgnored = true;
			//다시 탐색한다.
			bool bCanNotFindTree = true;
			fTreeDistance = TNumericLimits<float>::Max();
			for (int i = 0; i < trees.size(); ++i)
			{
				float distance = FVector::DistSquared(mAILocation, trees[i].mTree->GetActorLocation());
				//Ignored 상태인 tree는 탐색에 넣지 않는다.
				if (trees[i].bIgnored) continue;
				if (trees[i].mTree->CanHarvest)
				{
					if (distance < fTreeDistance)
					{
						fTreeDistance = distance;
						TargetTreeNum = i;
						bCanNotFindTree = false;
					}
				}
			}

			if (bCanNotFindTree)
			{
				//반복문을 다 돌았는데 (모든 나무탐색을 했는데)
				// 내가 갈 나무가 아무것도 존재하지 않다면
				//랜덤무브를 해줘야함. 아직 코딩하진않겠음.
				//현재는 그냥 Failed처리
				trees.clear();
				fTreeDistance = TNumericLimits<float>::Max();
				return EBTNodeResult::Failed;
			}
			//재사용을 위한 초기화.
			fTreeDistance = TNumericLimits<float>::Max();
		}
		else {
			//방해하는 적이 없다면 탈출해서 나무로 달려간다.

			if (AIController)
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(AAIController_Custom::TreePosKey, trees[TargetTreeNum].mTree);
				FVector goal = FVector(trees[TargetTreeNum].mTree->GetActorLocation());
				//goal.Y += 70;
				//UAIBlueprintHelperLibrary::SimpleMoveToLocation(ControllingPawn->GetController(), goal);
				UAIBlueprintHelperLibrary::SimpleMoveToLocation(ControllingPawn->GetController(), trees[TargetTreeNum].mTree->GetActorLocation());
			}
			else if (smartAIController)
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(AAI_Smart_Controller_Custom::TreePosKey, trees[TargetTreeNum].mTree);
				FVector goal = FVector(trees[TargetTreeNum].mTree->GetActorLocation());
				UAIBlueprintHelperLibrary::SimpleMoveToLocation(ControllingPawn->GetController(), trees[TargetTreeNum].mTree->GetActorLocation());
			}
			
			break;
		}
	}

단순히 복붙을 했더니 코드가 길다. 설명하겠다.

 

먼저 LineTraceSingleForObjects 함수로 AI위치에서부터~ 제일 가까운 나무까지로 광선을 쏜다.

 

이때 AI몸 안에서부터 광선이 시작되니 IgnoreActor에 자기자신을 넣어주었다. (Ignore 하기 싫으면 광선 시작위치를 Forward Vector로 앞으로 조금 꺼내도 된다)

 

그리고 히트 될 오브젝트는 ECC::Pawn으로 해서 캐릭터만 히트가능할 수 있게 했다. (방해하는 애는 Pawn말고는 없으니까..)

 

그래서 만약 히트가 된다면? HitResult에 값이 쌓이게 되는데 

 

그럼 이 히트가 되버린 나무는 아 여기로 가면 안되겠구나! 하고 bIgnore를 true로 해서 무시해버리는것이다 ! 

 

그리고 다~시 나무를 탐색하는것부터 시작하게 된다.

 

아무도 안부딪히면 거기로 무브

 

이런 알고리즘인데

 

문제가 있기는 하다 

 

1. AI 중심에서 1개의 광선만 쏘다보니 몸 중앙이아니라 왼쪽오른쪽으로 연행하듯이 길을 막아버리면 여전히 멈춘다는 것...

 

해결하려면 몸의 맨 왼쪽, 몸의 가운데, 몸의 맨 오른쪽 을 시작점을 두고 도착지점까지 쏘는게 정확도를 늘릴 수 있다!

 

2. 모든 나무를 다 방해한다면 ? 

 

멈춰버리게 된다...! 

하지만 이건 혼자서는 힘들고 여러명이 합심해서 (그것도 개인전인 게임에서) AI를 그렇게 괴롭히고 싶을까..?

 

그정도로 노력해서 AI를 괴롭히고 싶다면.. 말리진 않겠다. 플레이어의 자유니까.

이부분은.. AI가 아니더라도 플레이어라도 방해받으면 못빠져나갈텐데..? 해결법은 딱히 없는듯 

'개발 > Unreal Engine 4' 카테고리의 다른 글

UE4에서의 이동 동기화.  (1) 2023.09.09

c++관련 내용 쓸 예정

'개발 > C++' 카테고리의 다른 글

ConnectEx를 적절하게 써보자  (1) 2023.10.15
lua coroutine을 이용한 퀘스트 스크립트  (1) 2023.07.12

+ Recent posts