250716

lililllilillll·2025년 7월 16일

개발 일지

목록 보기
234/350

✅ What I did today


  • 자기소개서 리뉴얼
  • R&D : Fishnet
  • 백준


🛠️ R&D : Fishnet


Remote Procedure Calls (RPCs)

RPCs는 보내진 객체와 같은 객체에 도착하는 통신이다.
ServerRpc, ObserversRpc, TargetRpc 세 종류가 있다.
RPCs는 객체에 종속되어 있기 때문에, NetworkBehaviour를 상속받은 스크립트에서만 호출 가능하다.

ServerRpc

  • 클라가 서버에서 로직을 실행할 수 있게 한다.
  • 클라가 ServerRpc 메서드를 호출하면, 메서드를 실행할 데이터가 서버에 보내진다.
  • ServerRpc를 사용하기 위해선 클라가 활성화돼있고, 메서드는 ServerRpc Attribute를 갖고 있어야 한다.
private void Update()
{
    //If owner and space bar is pressed.
    if (base.IsOwner && Input.GetKeyDown(KeyCode.Space))
        RpcSendChat("Hello world, from owner!");        
}

[ServerRpc]
private void RpcSendChat(string msg)
{
    Debug.Log($"Received {msg} on the server.");
}
  • 기본적으론 객체의 owner만 ServerRpc를 보낼 수 있다.
  • RequireOwnership을 false로 설정하면 다른 클라도 호출 가능하다.
  • RPC 호출할 때 마지막 인자로 NetworkConnection type을 null로 두면 어떤 연결이 RPC를 호출하고 있는지 확인 가능. 그럼 NetworkConnection 인자에 자동으로 어떤 클라가 ServerRpc 호출중인지 채워진다.
[ServerRpc(RequireOwnership = false)]
private void RpcSendChat(string msg, NetworkConnection conn = null)
{
    Debug.Log($"Received {msg} on the server from connection {conn.ClientId}.");
}

ObserversRpc

  • 서버가 클라에서 로직을 실행할 수 있게 한다.
  • 관찰 중인 클라만 받아서 로직을 실행할 수 있다.
  • 옵저버들은 Network Observer 컴포넌트를 사용하여 설정한다.
  • ObserverRpc를 사용하려면 ObserverRpc attribute를 붙여야 한다.
private void FixedUpdate()
{
    RpcSetNumber(Time.frameCount);
}

[ObserversRpc]
private void RpcSetNumber(int next)
{
    Debug.Log($"Received number {next} from the server.");
}
  • ObserverRpc attribute의 BufferLast를 true로 설정하면 ObserversRpc를 통해 전송된 최신 값들이, 새로 접속한 클라에게도 자동으로 전달되도록 설정할 수 있다.
[ObserversRpc(ExcludeOwner = true, BufferLast = true)]
private void RpcSetNumber(int next)
{
    //This won't run on owner and will send to new clients.
    Debug.Log($"Received number {next} from the server.");
}

(owner에는 안 보내고 새 클라에 보내는 예제)

TargetRpc

  • 특정 클라에서 로직을 돌리게 한다. TargetRpc attribute를 붙이면 된다.
  • TargetRpc를 보낼 때 첫 번째 인자는 반드시 NetworkConnection이어야 한다. 이 연결이 데이터가 가는 곳이다.
private void UpdateOwnersAmmo()
{
    /* Even though this example passes in owner, you can send to
    * any connection that is an observer. */
    RpcSetAmmo(base.Owner, 10);
}

[TargetRpc]
private void RpcSetAmmo(NetworkConnection conn, int newAmmo)
{
    //This might be something you only want the owner to be aware of.
    _myAmmo = newAmmo;
}

Multi-Purpose Rpc

  • 하나의 메서드가 TargetRpc와 ObserversRpc 둘 다 여야 할 때 사용한다.
[ObserversRpc][TargetRpc]
private void DisplayChat(NetworkConnection target, string sender, string message)
{
    Debug.Log($"{sender}: {message}."); //Display a message from sender.
}

[Server]
private void SendChatMessage()
{
    //Send to only the owner.
    DisplayChat(base.Owner, "Bob", "Hello world");
    //Send to everyone.
    DisplayChat(null, "Bob", "Hello world");
}

Channels

  • Channeltype을 rpc의 마지막 인자로 넣어서 reliable 또는 unreliable하게 호출 가능하다.
  • 추가된 Channel 인자로 어떤 채널에 데이터가 도착할지 알 수도 있다.
private bool _reliable;
private void FixedUpdate()
{
    //Reliable or unreliable can be switched at runtime!
    _reliable = !_reliable;
    
    Channel channel = (_reliable) ? Channel.Reliable : Channel.Unreliable;
    RpcTest("Anything", channel);
}

/* This example uses ServerRpc, but any RPC will work.
* Although this example shows a default value for the channel,
* you do not need to include it. */
[ServerRpc]
private void RpcTest(string txt, Channel channel = Channel.Reliable)
{
    if (channel == Channel.Reliable)
        Debug.Log("Message received! I never doubted you.");
    else if (channel == Channel.Unreliable)
        Debug.Log($"Glad you got here, I wasn't sure you'd make it.");
}
  • 만약 ServerRpc이고 NetworkConnection을 기본값 null로 사용하고 있다면 Channel 인자는 끝에서 두 번째 NetworkConnection 전에 반드시 들어가야 한다.
/* Example of using Channel with a ServerRpc that
* also provides the calling connection. */
[ServerRpc(RequireOwnership = false)]
private void RpcTest(string txt, Channel channel = Channel.Reliable, NetworkConnection sender = null)
{
    Debug.Log($"Received on channel {channel} from {sender.ClientId}.");
}

RunLocally

  • 모든 RPC type들은 RunLocally 필드를 사용할 수 있다.
  • RunLocally가 true면 메서드가 보내질 때, 호출한 쪽에서도 실행된다.
//Keeping in mind all RPC types can use RunLocally.
[ServerRpc(RunLocally = true)]
private void RpcTest()
{
    //This debug will print on the server and the client calling the RPC.
    Debug.Log("Rpc Test!");
}

DataLength

  • 데이터 엄청 많이 보내려면 데이터의 RPC attribute에서 최대 크기를 설정해야 한다. 그래야 resizing이랑 allocation을 방지해서 퍼포먼스 좋아짐.
//This implies we know the data will never be larger than 3500 bytes.
//If the data is larger then a resize will occur resulting in garbage collection.
[ServerRpc(DataLength = 3500)]
private void ServerSendBytes(byte[] data) {}

SyncTypes

  • 서버가 값을 바꾸면 네트워크에 있는 클라들에 자동으로 싱크함.
  • SyncVar, SyncDictionary, SyncList, custom SyncTypes 등 여러 종류가 있다.
  • SyncType 값 변경하면 바꾼 놈만 간다. 예를 들어, SyncList에 값 10개 있고 하나 추가했으면 추가한 하나만 보내진다.
  • SyncType들은 custom 자료형에 대해 자동으로 직렬화해준다.
  • SyncVar는 OnDestroy 이전에 초기화되므로, 거기서 접근 못 함.
  • Awake에서 SyncType 변경하면 서버와 클라 모두에 영향 미쳐서 별도 싱크 필요 없음. 초기화 이후에 싱크 맞추고 싶으면 OnStartServer 사용.
  • SendRate를 0f로 설정하면 SyncType들을 매 network tick에 보낼 수 있게 한다.

Host Client Limitations

클라와 서버를 동일 빌드에서 돌리면 약간의 제한이 발생함.
호스트 클라일 때 asServer 인자가 false면 이전값이 null이나 현재값으로 찍힐 수 있다.

// This example assumes you are acting as the host client.
// We will imagine previous value was 10, and next is 20.
private void _mySyncVar_OnChange(int prev, int next, bool asServer)
{
    // Only run the logic using the previous value if being called
    // with asServer true, or if only the client is started.
    if (asServer || base.IsClientOnlyStarted)
        DoSomething(prev, next);
}


⚔️ 백준


3197 백조의 호수

틀린 풀이

bool IsValidXY(int& R, int& C, vector<string>& lake, int& newx, int& newy)
{
	if (0 <= newx && newx < R && 0 <= newy && newy < C) return true;
	else return false;
}

void B3197::Solution()
{
	fastio;

	// 얼음 녹인 뒤에 오리 한 쪽에서부터 갈 수 있는지 체크.
	// 근데 이전 체크에서 얼음이었던 곳에서부터 bfs 다시 시작함.
	int R, C;
	string s;
	cin >> R >> C;
	vector<string> lake(R);
	pii duck;
	queue<pii> iceq;
	vector<vector<bool>> visited(R, vector<bool>(C));
	vector<vector<bool>> willmelt(R, vector<bool>(C));
	for (int i = 0; i < R; i++)
		cin >> lake[i];

	vector<int> dx = { 1,-1,0,0 };
	vector<int> dy = { 0,0,1,-1 };

	for (int i = 0; i < R; i++) {
		for (int j = 0; j < C; j++) {
			char c = lake[i][j];
			if (c == 'X') {
				for (int k = 0; k < 4; k++) {
					int newx = i + dx[k]; int newy = j + dy[k];
					if (IsValidXY(R, C, lake, newx, newy)
						&& lake[newx][newy] == '.') {
						iceq.push({ i,j });
						willmelt[i][j] = true;
						break;
					}
				}
			}
			else if (c == 'L') {
				duck = { i,j };
			}
		}
	}

	bool done = false;
	int day = 0;
	queue<pii> duckq;
	queue<pii> nextdq;
	duckq.push(duck);
	visited[duck.first][duck.second] = true;
	while (true)
	{
		while (!nextdq.empty()) {
			duckq.push(nextdq.front()); nextdq.pop();
		}

		while (!duckq.empty()) {
			pii d = duckq.front(); duckq.pop();
			for (int i = 0; i < 4; i++) {
				int newx = d.first + dx[i];
				int newy = d.second + dy[i];
				if (IsValidXY(R, C, lake, newx, newy)
					&& !visited[newx][newy])
				{
					if (lake[newx][newy] == '.') {
						visited[newx][newy] = true;
						duckq.push({ newx,newy });
					}
					else if (lake[newx][newy] == 'X'
						&& willmelt[newx][newy]) {
						visited[newx][newy] = true;
						nextdq.push({ newx, newy });
					}
					else if (lake[newx][newy] == 'L') {
						done = true;
						break;
					}
				}
			}
			if (done) break;
		}
		if (done) break;

		int iceqlen = iceq.size();
		while (iceqlen--) {
			pii ice = iceq.front(); iceq.pop();
			lake[ice.first][ice.second] = '.';
			for (int i = 0; i < 4; i++) {
				int newx = ice.first + dx[i];
				int newy = ice.second + dy[i];
				if (IsValidXY(R, C, lake, newx, newy)
					&& !willmelt[newx][newy]
					&& lake[newx][newy] == 'X')
				{
					willmelt[newx][newy] = true;
					iceq.push({ newx,newy });
				}
			}
		}

		day++;
	}
	cout << day;
}

45%에서 시간 초과. 내일 다시.



profile
너 정말 **핵심**을 찔렀어

0개의 댓글