워크스페이스의 MyDesk를 우클릭 후 Create Model을 선택해줍니다.

저는 Model - Skill 폴더를 만들어놔서 해당 폴더에 모델을 만들었습니다.

 

 

SpriteRendererComponent를 추가 후 SpriteRUID에 원하는 화살 애니메이션을 넣습니다.

저는 Resource Storage - MSW - animationclip - Item에서 arrow를 검색 후 파란색 화살을 사용했습니다.

 

 

TransformComponent와 TriggerComponent도 추가하여 크기와 Collider Size를 조절해줍니다.

파란색 화살 기준으로 Scale (1, 1, 1), Collider BoxSize(0.5, 0.2)로 설정하니 괜찮아 보이네요.

 

TriggerComponent의 CollisionGroup에 PlayerProjectile을 추가한 후 Matrix설정을 해줍니다.

 

 

 

몬스터 피격 시 이펙트도 Resource Storage에 가서 마음에 드는걸로 사용하면 됩니다.

저는 animationclip - Skill - 공격 검색 후 3번째 이펙트를 사용했습니다.

 

PlayerUnitProjectile 컴포넌트를 만들어 화살 모델에게 넣어줍니다.

OnUpdate에서 Speed만큼 x축을 이동시켜 화살이 앞으로 나아가도록 하였고 TriggerEnter 이벤트로 몬스터와 부딪혔을 때 데미지를 주고 공격 이펙트를 보여준 후 파괴되도록 하였습니다.

 

공격 이펙트 부분은 현재 안나오고 있습니다. 다음 글에서 수정하겠습니다.

 

PalyerUnitAttack 컴포넌트를 만들어서 헬레나에게 넣었습니다.

ProjectileModel에는 화살 모델을 우클릭하여 EntityID를 복사하여 넣었습니다.

OnUpdate에서는 AttackTimer에 시간을 더해나가 공격이 가능한지 체크하고 Attack함수에서는 공격이 가능하다면 화살 모델을 소환하고 데미지를 넘겨주도록 하였습니다.

 

PlayerUnitState의 OnUpdate 부분에서 공격함수를 호출하도록 하였습니다.

이동할 때나 공격할 때 그에 맞는 애니메이션이 재생되도록 하였는데 현재 제대로 작동하지 않습니다.

다음 글에서 수정해보겠습니다.

 

게임을 실행해 보니 헬레나의 공격 범위 내에 들어오면 화살이 발사되고 위에서 넘겨준 데미지 값 100이 제대로 들어갔습니다.

 

주황 버섯이 데미지를 받아도 죽지 않아 확인해보니 EnemyState에 피격 이벤트가 없었습니다.

플레이어 유닛와 같이 넣어줍니다.

 

주황 버섯의 Hp를 100으로 설정 후 실행하니 화살 한 방에 제대로 죽고 있습니다.

 

다음 글에서는

- 헬레나 공격 시 공격 애니메이션

- 화살이 주황 버섯 공격 시 이펙트

- 헬레나 죽을 때 애니메이션

- 주황 버섯 죽을 때 애니메이션

- 플레이어의 데미지스킨과 몬스터의 데미지스킨 구별되게 수정

을 해보겠습니다.

문제 출처 : https://www.acmicpc.net/problem/11438

 

11438번: LCA 2

첫째 줄에 노드의 개수 N이 주어지고, 다음 N-1개 줄에는 트리 상에서 연결된 두 정점이 주어진다. 그 다음 줄에는 가장 가까운 공통 조상을 알고싶은 쌍의 개수 M이 주어지고, 다음 M개 줄에는 정

www.acmicpc.net

트리를 만들고 깊이를 체크해준다.

이후 깊이에 맞게 부모를 설정해준다.

같은 깊이까지 끌어올리고 두 수가 같은지 비교후 같다면 출력, 다르다면 한단계 올라가기가 생각 한 풀이방법이다.

 

#define MAX_NODE 100001
#define MAX_DEPTH 17 // 2^17 > 100000
int parent[MAX_NODE][MAX_DEPTH];
int depth[MAX_NODE];
bool check[MAX_NODE];
vector<int> tree[MAX_NODE];

MAX_NODE는 문제에서 주어진 정점의 최대 개수를, MAX_DEPTH는 log2(100001) 약 16.6이므로 17을 설정해 주었다.

부모를 담을 배열과 깊이를 담을 배열, dfs에서 체크를 해줄 배열, 트리틑 만들 tree벡터를 선언해주었다.

 

int n, m;
cin >> n;
for (int i = 1; i < n; i++) {
	int u, v;
	cin >> u >> v;
	tree[u].push_back(v);
	tree[v].push_back(u);
}
DFS(1, 0);

두 수를 입력 받고 tree에 저장한 후 DFS를 돌려준다.

루트는 1번 노드이며 루트의 깊이는 0이다.

 

void DFS(int node, int d) {
	check[node] = true;
	depth[node] = d;
	for (int i = 0; i < tree[node].size(); i++) {
		int next = tree[node][i];
		if (check[next]) continue;
		parent[next][0] = node;
		DFS(next, d + 1);
	}
}

방문한 노드인지 check배열에 체크 후, depth배열에 깊이를 저장해준다.

parent의[][0]에 한칸 위의 부모를 저장해준다.

 

void setParent() {
	for (int j = 1; j < MAX_DEPTH; j++) {
		for (int i = 1; i < MAX_NODE; i++) {
			parent[i][j] = parent[parent[i][j - 1]][j - 1];
		}
	}
}
parent[i][j] = parent[parent[i][j-1]][j-1]

node   :      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
parent[.][0]: 0 1 1 2 2 2 3 3 4 4  5  5  7  7  11
parent[.][1]: 0 0 0 1 1 1 1 1 2 2  2  2  3  3  5
parent[.][2]: 0 0 0 0 0 0 0 0 1 1  1  1  1  1  2
...

setParent를 돌리면 위와 같은 형태로 되어있을 것이다.

0에는 부모, 1에는 조부모.. 형태로 들어가 있다.

 

int LCA(int x, int y) {
	if (depth[x] > depth[y]) swap(x, y);
	for (int i = MAX_DEPTH - 1; i >= 0; i--) {
		if (depth[y] - depth[x] >= (1 << i)) {
			y = parent[y][i];
		}
	}
	if (x == y) return x;
	for (int i = MAX_DEPTH - 1; i >= 0; i--) {
		if (parent[x][i] != parent[y][i]) {
			x = parent[x][i];
			y = parent[y][i];
		}
	}
	return parent[x][0];
}

이후 LCA를 돌려준다.

우선 2번째 매개변수 y쪽이 더 높은 depth를 가지도록 순서를 변경해준다.

y가 x와 같은 깊이를 가질 때까지 끌어올려주고 y를 해당 깊이의 조상으로 값을 변경해준다.

x와 y가 같다면 이가 최소 공통 조상이므로 return 해준다.

같지 않다면 더 위로 올라가 최소 공통 조상을 찾아야 한다.

한칸씩 위로 올라가며 둘이 같지 않다면 각각 부모의 값으로 변경해준다.

둘이 같다면 최소 공통 조상이므로 이를 return 해준다.

 

#include <iostream>
#include <vector>
using namespace std;

#define MAX_NODE 100001
#define MAX_DEPTH 17 // 2^17 > 100000
int parent[MAX_NODE][MAX_DEPTH];
int depth[MAX_NODE];
bool check[MAX_NODE];
vector<int> tree[MAX_NODE];

void DFS(int node, int d) {
	check[node] = true;
	depth[node] = d;
	for (int i = 0; i < tree[node].size(); i++) {
		int next = tree[node][i];
		if (check[next]) continue;
		parent[next][0] = node;
		DFS(next, d + 1);
	}
}

void setParent() {
	for (int j = 1; j < MAX_DEPTH; j++) {
		for (int i = 1; i < MAX_NODE; i++) {
			parent[i][j] = parent[parent[i][j - 1]][j - 1];
		}
	}
}

int LCA(int x, int y) {
	if (depth[x] > depth[y]) swap(x, y);
	for (int i = MAX_DEPTH - 1; i >= 0; i--) {
		if (depth[y] - depth[x] >= (1 << i)) {
			y = parent[y][i];
		}
	}
	if (x == y) return x;
	for (int i = MAX_DEPTH - 1; i >= 0; i--) {
		if (parent[x][i] != parent[y][i]) {
			x = parent[x][i];
			y = parent[y][i];
		}
	}
	return parent[x][0];
}

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	int n, m;
	cin >> n;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		tree[u].push_back(v);
		tree[v].push_back(u);
	}
	DFS(1, 0);
	setParent();
	cin >> m;
	for (int i = 1; i < m; i++) {
		int u, v;
		cin >> u >> v;
		cout << LCA(u, v) << "\n";
	}
	return 0;
}

전체 코드

 

알고리즘

- 자료 구조

- 트리

- 최소 공통 조상

- 희소 배열

'알고리즘 > 백준' 카테고리의 다른 글

[백준 1916] 최소비용 구하기 (C++)  (0) 2023.08.08
[백준 1238] 파티 (C++)  (0) 2023.08.07
[백준 1753] 최단경로 (C++)  (0) 2023.08.06
[백준 2293] 동전1 (C++)  (0) 2023.07.22
[백준 2042] 구간 합 구하기 (C++)  (0) 2023.07.21

상대적으로 쉬운 주황 버섯의 공격부터 구현해보겠습니다.

주황 버섯은 부딪혔을 때 데미지를 주기 때문데 TriggerEvent에서 공격 효과를 넣어주어야 한다.

 

주황 버섯의 공격을 구현하기 전에 플레이어 유닛의 무적 시간을 먼저 설정 해주어야 한다.

주황 버섯과 충동 중일 때 데미지가 계속 들어온다면 플레이어 유닛은 바로 죽어버릴 것이다.

피격을 관리 하는 HitComponent를 Extend 하여 플레이어 유닛에게 무적 시간을 주겠습니다.

 

 

Workspace의 BaseEnvironment - NativeScripts - Component를 보면 여러 컴포넌트가 있는데 그 중

HitComponent를 찾아 Extend를 해주고 이름을 PlayerUnitHit로 지어주었다.

 

HitComponent에서 피격 가능한 상태인지 판단하는 IsHitTarget함수가 있는데 이를 override하여 무적 시간을 주겠습니다.

_UtilLogic.ElapsedSeconds를 하면 게임이 시작 된 후 흐른 시간을 불러올 수 있다.

만약 마지막 타격 시간에 무적 시간을 더하였을 때 현재 시간보다 작다면 피격 가능한 상태이니 true를 return 해준다.

아니라면 false를 return 한다.

 

플레이어 유닛의 피격 판정은 PlayerUnitHit 컴포넌트를 사용할 것이기 때문에 헬레나에서 HitComponent를 삭제 후 PlayerUnitHit 컴포넌트를 붙여주었다.

무적 시간인 ImmuneCooldown는 1로 설정하였다.

 

 

 

다음은 주황 버섯의 공격 부분을 설정해 보겠습니다.

TriggerEvent는 Enter, Stay, Leave가 있는데 각각 충돌했을 때, 충돌 중일 때, 충돌이 끝났을 때이다.

주황 버섯과 플레이어 유닛이 충돌 중이라면 일정 주기마다 데미지를 주어야 하기 때문에 TriggerStayEvent를 사용하였다.

충돌 중 이벤트가 발생하였을 때 CollisionGroup을 확인하여 Player이고 해당 유닛이 피격 가능 한 상태인지 IsHitTarget을 사용하여 확인한다.

피격 가능 상태라면 플레이어 유닛에게 OnHit를 통해 데미지를 준다.

 

다음은 플레이어 유닛 쪽에서 피격을 처리해주어야 한다.

현재 hp에서 들어온 데미지만큼 hp를 깎고, 남은 hp가 0보다 작다면 해당 유닛을 파괴한다.

 

게임을 플레이해보면 주황 버섯과 헬레나가 부딪혔을 때 주황 버섯에게 설정한 데미지 50이 제대로 들어가고 있습니다.

헬레나의 hp는 100으로 설정했기 때문에 2번 데미지를 입으면 파괴되는 것까지 확인 했습니다.

이전 글에서 TriggerComponent를 사용해서 구현을 하려고 했더니 잘 안됐었다.

찾아보니 CollisionService라는 충돌 관련 기능을 제공하는 서비스가 있었다.

헬레나의 TriggerComponet 사이즈를 모델에 딱 맞게 수정한 후

 

 

UnitState를 다음과 같이 수정하였다.

 

몹 감지에 사용할 CircleShape 변수를 만들고 OnBeginPlay에서 반지름 1짜리 원모양으로 만들어주었다.

CircleShape를 모델 포지션에 맞추고 CollisionService에서 GetSimulator를 통해 시뮬레이터를 가지고 온다.

Simulator는 여러가지 충돌 검사 기능을 제공한다고 하는데 그 중에서 Overlap을 이용해서 몬스터를 감지하려고 한다.

첫번째 변수로는 충돌 검사를 하고 싶은 CollisionGroup을 ("Moster" 라고 string 형식으로 적어도 된다), 두번째 변수로는 Shape를 넣어준다.

이렇게 입력을 해주면 모양과 겹치는 첫번째 Component를 찾아 반환한다고 한다.

만약 nil이라면 충돌한 Component가 없으니 이동속도를 1.5로 하여 이동을 하게 하고 만약 nil이 아니라면 충돌한 몬스터가 있다는 뜻이니 이동속도를 0으로 하여 멈추게 하였다.

 

EnemyState도 위와 같이 수정을 해준 후 실행을 해보니

충돌을 했을 때 제대로 멈추는 것을 확인 할 수 있었다.

 

헬레나는 위와 같이 일정 범위 안에 몬스터가 있는 경우 멈춘 후에 공격을 하면 된다.

하지만 주황 버섯과 같이 부딪혀서 데미지를 주는 몬스터인 경우 유닛 앞에서 멈추어서는 데미지를 줄 수 없다.

이전 글에서 생각했던 것처럼 플레이어 유닛과 충돌 시 AIChaseComponent의 Target을 플레이어 유닛으로 바꾸어서 부딪히게끔 해주어야 한다.

 

 

EnemyState 컴포넌트를 약간 수정하였다.

범위 안에 부딪힌 플레이어 유닛이 없다면 플레이어 기지를 타겟으로 이동을 하고 부딪힌 플레이어 유닛이 있다면 해당 유닛을 타겟으로 하도록 하였다.

PlayerBase의 경우 EnemyState의 Property에 추가 & 설정 해두었다.

 

이후 게임을 실행해보니 헬레나는 주황 버섯이 앞에 있는 경우 이동을 멈추었고, 주황 버섯은 플레이어 기지를 향해 이동하다가 헬레나를 만나면 헬레나를 향해 이동하는 것을 볼 수 있었다.

 

다음 글에서는 헬레나가 멈췄을 때 화살 날리기, 주황 버섯이 화살에 맞았을 때 HP 감소, 헬레나가 주황 버섯과 부딪혔을 때 HP 감소 기능을 구현해 보겠습니다.

문제 출처 : https://www.acmicpc.net/problem/2293

 

2293번: 동전 1

첫째 줄에 n, k가 주어진다. (1 ≤ n ≤ 100, 1 ≤ k ≤ 10,000) 다음 n개의 줄에는 각각의 동전의 가치가 주어진다. 동전의 가치는 100,000보다 작거나 같은 자연수이다.

www.acmicpc.net

다이나믹 프로그래밍으로 풀었다.

 

int n, k;
cin >> n >> k;
vector<int> coin(n + 1, 0);
vector<int> dp(k + 1, 0);
dp[0] = 1;

n과 k를 입력 받고 사이즈에 맞게 vector를 선언해준다.

coin은 n개의 동전 값을 받아 저장하고, dp는 금액을 만들 수 있는 방법의 수를 저장할 것이다.

dp[0]이 1인 이유는 0원을 만드는 방법은 아무것도 사용하지 않는 한가지 방법만 있기 때문이다.

 

for (int i = 0; i < n; i++) {
	cin >> coin[i];
}
for (int i = 0; i < n; i++) {
	for (int j = coin[i]; j <= k; j++) {
		dp[j] += dp[j - coin[i]];
	}
}
cout << dp[k];

동전의 가치를 입력 받고 2중 for문을 통해 각각의 동전마다 다이나믹 프로그래밍을 수행한다.

dp[j] += dp[j - coin[i]]는 j원을 만들 수 있는 방법의 수 dp[j]를 계산하는 것이다.

coin[i]는 현재 다이나믹 프로그래밍을 수행하는 동전의 가치이다.

j - coin[i]는 현재 만드려하는 j원에서 코인의 가치를 뺀 것으로 j - coin[i]를 만드는 방법의 수 dp[j - coin[i]에 coin[i]를 추가하면 j를 만드는 방법의 수가 되는 것이다.

 

예시와 같이 3 10 1 2 5를 입력했다고 했을 때

dp: 1 0 0 0 0 0 0 0 0 0 0 (index는 목표하는 금액을 의미합니다.)

초기에는 위와 같은 상태이다.

 

 

dp: 1 1 1 1 1 1 1 1 1 1 1

1원 동전에 대해 다이나믹 프로그래밍을 수행한 결과이다.

 

dp: 1 1 2 2 3 3 4 4 5 5 6

2원 동전에 대해 다이나믹 프로그래밍을 수행한 결과이다.

예를 들어 2원을 만드는 방법에는 1원 2개를 쓰는 방법과 2원 1개를 쓰는 방법, 총 2가지가 있을 것이다.

3원을 만드는 방법에는 1원 3개를 쓰는 방법과 2원 1개, 1원 1개를 쓰는 방법, 총 2가지가 있을 것이다.

4원을 만드는 방법은 2원을 만드는 방법인 1원2개, 2원 1개에서 2원을 추가하여 (1, 1, 2), (2, 2)와 1원만을 사용한 (1, 1, 1, 1) 총 3가지 방법이 있을 것이다.

위와 같이 j원을 만드는 방법은 현재 저장되어 있는 j원을 만드는 방법의 수에서 j - coin[i]을 만드는 방법의 수를 더해 나가면 구할 수 있다.

 

dp: 1 1 2 2 3 4 5 6 7 8 10

5원 동전에 대해 다이나믹 프로그래밍을 수행한 결과이다.

 

모든 동전에 대해 다이나믹 프로그래밍을 수행했다면 k원을 만드는 방법의 가지 수인 dp[k]를 출력해준다.

 

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	int n, k;
	cin >> n >> k;
	vector<int> coin(n + 1, 0);
	vector<int> dp(k + 1, 0);
	dp[0] = 1;
	for (int i = 0; i < n; i++) {
		cin >> coin[i];
	}
	for (int i = 0; i < n; i++) {
		for (int j = coin[i]; j <= k; j++) {
			dp[j] += dp[j - coin[i]];
		}
	}
	cout << dp[k];
	return 0;
}

전체코드

 

알고리즘

- 다이나믹 프로그래밍

'알고리즘 > 백준' 카테고리의 다른 글

[백준 1916] 최소비용 구하기 (C++)  (0) 2023.08.08
[백준 1238] 파티 (C++)  (0) 2023.08.07
[백준 1753] 최단경로 (C++)  (0) 2023.08.06
[백준 11438] LCA2 (C++)  (0) 2023.07.25
[백준 2042] 구간 합 구하기 (C++)  (0) 2023.07.21

문제 출처 : https://www.acmicpc.net/problem/2042

 

2042번: 구간 합 구하기

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)과 M(1 ≤ M ≤ 10,000), K(1 ≤ K ≤ 10,000) 가 주어진다. M은 수의 변경이 일어나는 횟수이고, K는 구간의 합을 구하는 횟수이다. 그리고 둘째 줄부터 N+1번째 줄

www.acmicpc.net

알고리즘 분류에 세그먼트 트리가 있어 세그먼트 트리를 활용하여 풀어보았다.

세그먼트 트리는 이진 트리이며 배열의 특정 구간에 대한 정보를 관리하기에 좋다.

리프 노드에 값을 입력 받고 부모 노드에 특정 정보를 저장하여 관리한다.

  3 
 / \
1   2

해당 예에서는 리프 노드에 1, 2를 저장하고 부모 노드에는 두 값의 합인 3을 저장하였다.

 

vector<long long> arr;
vector<long long> segtree;

arr에는 입력받은 값을 저장할 것이고 segtree에는 해당 값을 트리 형태로 저장 할 것이다.

입력으로 들어오는 수가 int형을 넘기 때문에 long long으로 선언하였다.

 

	int n, m, k, a, b;
	long long c;
	cin >> n >> m >> k;
	arr.resize(n);
	for (int i = 0; i < n; i++) {
		cin >> arr[i];
	}
	int h = (int)ceil(log2(n));
	int tree_size = (1 << (h + 1));
	segtree.resize(tree_size);
	init(1, 0, n - 1);

문제에서 주어진대로 n, m, k를 입력 받고 arr의 사이즈를 n으로 설정하였다.

h는 트리의 높이이다.

트리의 높이는 배열의 개수 n에 따라 결정 된다.

레벨이 오르면 이전 레벨의 2배가 되는 노드를 저장할 수 있으므로 트리의 높이는 log2(n)이 된다.

n이 2의 제곱이 아니더라도 모든 배열의 수를 저장해야 하므로 ceil을 사용해서 올림처리 해준다.

높이가 h인 트리의 노드의 수는 2^(h + 1) - 1이다.

트리 사이즈를 2^(h + 1)로 설정하여 첫 노드의 인덱스로 1을 사용할 수 있도록 하였다.

 

long long init(int node, int start, int end) {
	if (start == end) return segtree[node] = arr[start];
	int mid = (start + end) / 2;
	return segtree[node] = init(node * 2, start, mid) + init(node * 2 + 1, mid + 1, end);
}

init 함수를 통해 트리를 만들어준다.

node에는 인덱스, start에는 배열의 시작 인덱스, end에는 배열의 끝 인덱스를 넣어준다.

만약 start와 end가 같아면 원소가 1개인 것이므로 segtree[node]에 arr[start]를 넣어주고 끝낸다.

아니라면 반으로 쪼개어 재귀 호출을 해준다.

이 때 구간합을 구해야 하므로 왼쪽 자식 값과 오른쪽 자식 값의 합을 저장해준다.

 

                  15
               /      \
            6            9
          /   \        /   \
        3      3      4     5
       / \ 
      1   2

예제의 1, 2, 3, 4, 5를 넣고 init을 돌렸다면 위와 같은 형태로 저장이 되어있을 것이다.

 

	for (int i = 0; i < m + k; i++) {
		cin >> a >> b >> c;
		b--;
		if (a == 1) {
			long long diff = c - arr[b];
			arr[b] = c;
			update(1, 0, n - 1, b, diff);
		}
		else if (a == 2) {
            c--;
			cout << sum(1, 0, n - 1, b, c) << endl;
		}
	}

트리를 완성 했다면 세 수를 입력받고 a가 1인 경우 b(1 ≤ b ≤ N)번째 수를 c로 바꾸고 a가 2인 경우에는 b(1 ≤ b ≤ N)번째 수부터 c(b ≤ c ≤ N)번째 수까지의 합을 구하여 출력 해준다.

a가 1인 경우 b번째 수를 c로 바꾸는 것 부터 확인을 해보면 c와 b번째 수의 차를 diff에 저장해두고 b번째 수를 c로 바꾸어준다.

이후 update 함수를 호출하여 b번째 수를 포함하는 구간 값을 업데이트 해준다.

 

void update(int node, int start, int end, int idx, long long diff) {
	if (idx < start || idx > end) return;
	segtree[node] += diff;
	if (start != end) {
		int mid = (start + end) / 2;
		update(node * 2, start, mid, idx, diff);
		update(node * 2 + 1, mid + 1, end, idx, diff);
	}
}

만약 인덱스가 start보다 작거나 end보다 크다면 구간을 벗어간 것이므로 return 해준다.

구간에 포함 된다면 해당 인덱스 값에 diff를 더해준다.

만약 start와 end가 다르다면 자식 노드가 존재하는 것이므로 자식노드도 업데이트 해준다.

 

                  18
               /      \
            9            9
          /   \        /   \
        3      6      4     5
       / \ 
      1   2

예제에서의 1 3 6을 입력하여 업데이트 하면 다음과 같이 업데이트 되어있을 것이다.

 

 

long long sum(int node, int start, int end, int left, int right) {
	if (left > end || right < start) return 0;
	if (left <= start && end <= right) return segtree[node];
	int mid = (start + end) / 2;
	return sum(node * 2, start, mid, left, right) + sum(node * 2 + 1, mid + 1, end, left, right);
}

a가 2라면 구간합을 출력해야한다.

구간의 시작 인덱스를 left, 끝 인덱스를 end라고 했을 때, left가 end보다 크거나 right 가 start보다 작다면 구간을 벗어난 것이므로 0을 return 해준다. 0을 return하는 이유는 덧셈의 항등원이기 때문이다.

만약 left가 start이하이고 right가 end이상이라면 해당 구간을 포함하기 때문에 해당 인덱스 값을 return 해주면 된다.

만약 구간을 완전히 포함하지 못한다면 반을 쪼개어 재귀 호출을 해준다.

 

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

vector<long long> arr;
vector<long long> segtree;

long long init(int node, int start, int end) {
	if (start == end) return segtree[node] = arr[start];
	int mid = (start + end) / 2;
	return segtree[node] = init(node * 2, start, mid) + init(node * 2 + 1, mid + 1, end);
}

long long sum(int node, int start, int end, int left, int right) {
	if (left > end || right < start) return 0;
	if (left <= start && end <= right) return segtree[node];
	int mid = (start + end) / 2;
	return sum(node * 2, start, mid, left, right) + sum(node * 2 + 1, mid + 1, end, left, right);
}

void update(int node, int start, int end, int idx, long long diff) {
	if (idx < start || idx > end) return;
	segtree[node] += diff;
	if (start != end) {
		int mid = (start + end) / 2;
		update(node * 2, start, mid, idx, diff);
		update(node * 2 + 1, mid + 1, end, idx, diff);
	}
}

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	int n, m, k, a, b;
	long long c;
	cin >> n >> m >> k;
	arr.resize(n);
	for (int i = 0; i < n; i++) {
		cin >> arr[i];
	}
	int h = (int)ceil(log2(n));
	int tree_size = (1 << (h + 1));
	segtree.resize(tree_size);
	init(1, 0, n - 1);
	for (int i = 0; i < m + k; i++) {
		cin >> a >> b >> c;
		b--;
		if (a == 1) {
			long long diff = c - arr[b];
			arr[b] = c;
			update(1, 0, n - 1, b, diff);
		}
		else if (a == 2) {
            c--;
			cout << sum(1, 0, n - 1, b, c) << endl;
		}
	}
	return 0;
}

전체코드

 

알고리즘

- 자료 구조

- 세그먼트 트리

'알고리즘 > 백준' 카테고리의 다른 글

[백준 1916] 최소비용 구하기 (C++)  (0) 2023.08.08
[백준 1238] 파티 (C++)  (0) 2023.08.07
[백준 1753] 최단경로 (C++)  (0) 2023.08.06
[백준 11438] LCA2 (C++)  (0) 2023.07.25
[백준 2293] 동전1 (C++)  (0) 2023.07.22

플레이어 유닛의 공격범위 내에 몬스터가 들어왔다면 이동을 멈추고 공격을 해야합니다.

  • 플레이어 유닛의 공격범위 내에 몬스터가 들어왔다면 이동을 멈추기
  • 플레이어 유닛의 공격범위 내에 몬스터가 들어왔다면 화살을 발사하기
  • 몬스터가 화살에 맞았다면 일정 HP가 깎이고 화살 사라지기
  • 몬스터의 공격범위 내에 플레이어 유닛이 들어왔다면 이동을 멈추기
  • 플레이어 유닛과 몬스터가 부딪혔다면 플레이어의 HP가 깎이기
  • 플레이어 유닛의 공격범위 내에 몬스터가 없다면 다시 이동하기
  • 몬스터의 공격범위 내에 플레이어 유닛이 없다면 다시 이동하기

 

헬레나 모델에 TriggerComponent를 추가한 Edit 버튼을 누릅니다.

맵에 헬레나 모델이 추가되고 Collider의 Size를 눈으로 확인할 수 있습니다.

TriggerComponent의 ColliderOffset과 BoxSize를 적절히 조절하여 헬레나의 몬스터 감지 범위를 만들어줍니다.

해당 영역에 다른 TriggerComponent가 교차했을 때 순간을 제어할 수 있습니다.

 

 

TriggerComponent의 Collision Groups를 열어 Player와 Monster 그룹을 추가한 후 Player끼리와 Monster끼리는 충돌을 감지하지 못하게 설정해둡니다.

 

ColliderOffset과 BoxSize의 x축을 적당히 조절해서 몬스터를 감지하고 싶은 영역을 설정해줍니다.

 

주황 버섯쪽에도 똑같이 TriggerComponent를 추가한 후 Edit을 눌러 플레이어 유닛 감지 범위를 설정해줍니다.

주황 버섯은 원거리 공격을 하지 않고 부딪혔을 때만 데미지를 주기 때문에 주황 버섯의 크기에 딱 맞게 설정해주었습니다.

 

 

UnitState란 컴포넌트를 만들어서 방금 만든 영역안에 몬스터가 들어오면 이동을 멈추도록 해보겠습니다.

Entity Event Handler에 HandleTriggerStayEvent를 추가해줍니다.

해당 이벤트는 우리가 방금 만든 영역끼리 서로 충돌중일 때 발생됩니다.

UnitState는 플레이어 유닛에 들어갈 컴포넌트이니 CollisionGroups이 몬스터인 엔티티가 충돌중일 때 이동을 멈추도록 하겠습니다.

 

헬레나 모델에 UnitState 컴포넌트를 적용 후 플레이를 해보면 잘 이동하다가 범위내에 주황 버섯이 들어오면 움직임을 멈춥니다.

하지만 주황 버섯은 멈추지 않고 헬레나를 통과하고 있는데, EnemyState 컴포넌트도 똑같이 만들어서 주황 버섯에게 적용해주겠습니다.

주황 버섯은 범위 안에 플레이어 유닛이 들어온 경우에만 멈추어야 합니다.

CollisionGroups가 Player인 경우 InputSpeed를 0으로 설정해줍니다.

 

 

게임을 실행해보면 플레이어 유닛의 Collision과 충돌중일 때 멈추게 해놓은 탓에 헬레나의 몬스터 감지 범위 안에 들어오면 주황 버섯이 멈추어 버리네요

그리고 주황 버섯은 플레이어 유닛과 부딪혀서 데미지를 주어야 하는데 그냥 멈춰있으니 부딪히는 느낌이 나지 않습니다.

주황 버섯의 움직임 제어는 플레이어 유닛의 Collision과 충돌 시 AIChaseComponent의 TargetEntityRef를 해당 플레이어 유닛으로 바꾸거나 하는 식의 다른 방법을 사용해야 할 것 같습니다.

 

공격과 주황 버섯의 움직임 제어는 다음 글에 이어서 쓰겠습니다.

  • 스폰 타이밍 : 버튼을 눌렀을 때
  • 스폰 위치 : 플레이어 기지
  • 스폰 쿨타임 : 5초
  • 유닛 움직임 : 적 기지를 향해 일정한 속도로 이동한다.

좌측 상단의 UI 버튼을 누르면 UI 편집 페이지로 갈 수 있다.

 

UI 편집 페이지로 가면 Preset List의 UI 창이 뜬다.

버튼 모음을 선택하고 위치를 잡아준다.

 

나는 HP정보 위에 위치를 잡아주었다.

 

 

첫번째 가방 아이콘을 클릭한 후 버튼 기능을 도와주는 ButtonComponent를 추가하였다.

컴포넌트의 우측 상단 삼단바를 누르고 Open API Reference를 선택하면 컴포넌트에 대한 설명을 볼 수 있다.

 

Button에 관련 된 여러 이벤트가 있는데 이 중에서 ButtonClickEvent를 사용해서 버튼을 클릭했을 때 아군 유닛이 소환 되도록 해보겠습니다.

 

ButtonComponent가 있는 Entity에 UnitSpawn 컴포넌트를 새로 만들어서 추가 하면 Entity Event Handler에서 ButtonClickEvent를 사용 할 수 있습니다.

 

이전에 작성했던 EnemySpawn과 유사한 형태로 작성하였다.

OnUpdate에서 누적 시간을 계산해준다.

Entity Event Handler에 HandleButtonClickEvent를 추가하고 만약 누적 시간이 내가 설정 한 쿨타임보다 크다면 누적 시간을 초기화 한 후 유닛스폰 함수를 실행시켰다.

 

유닛스폰 함수는 이전 EnemySpawn 컴포넌트의 EnemySpawn함수와 동일하고 모델ID와 스폰 위치만 달라졌다.

스폰 위치는 플레이어 기지의 x축 위치를 사용하였고 모델은 헬레나를 사용하였다.

 

Preset List의 NPC 창에서 헬레나를 찾을 수 있었는데 해당 모델을 사용하니 움직이지 않고 애니메이션도 없는 헬레나가 나왔다.

 

이는 워크스페이스에 추가한 헬레나 모델을 클릭해보면 알 수 있는데 애니메이션 컴포넌트를 보면 가만히 서있는 애니메이션이 들어가 있어서 움직이지 않는 것이었다.

 

상단의 Resource Storage에 들어가보면 MSW에서 제공하는 여러 리소스를 사용할 수 있다.

 

animationClip의 NPC에서 헬레나를 검색한 후 걷는 애니메이션을 찾아 RUID를 복사한다.

 

복사한 RUID 값을 헬레나 모델의 애니메이션 컴포넌트의 Value에 붙여넣어 준다.

 

게임을 시작하고 5초가 지난 뒤 첫번째 가방 아이콘을 선택하면 플레이어 기지에서 헬레나가 소환된다.

걷는 애니메이션도 잘 재생된다.

하지만 제자리에서 걸을 뿐 적 기지를 향해 움직이지는 않는데 이는 저번 글에서 주황 버섯을 움직였던 것 처럼 AIChaseComponent를 추가하면 된다.

 

 

AIChaseComponent를 추가하고 DetedctionRange는 15, TargetEntityRef는 적 기지, UpdateAuthority는 Client를 선택한다.

MovementComponent도 추가하여 이동속도를 설정해준다.

 

게임을 시작하고 버튼을 눌러보면 헬레나가 소환되고 적 기지를 향해 걸어간다.

 

  • 스폰 타이밍 : 버튼을 눌렀을 때
  • 스폰 위치 : 플레이어 기지
  • 스폰 쿨타임 : 5초
  • 유닛 움직임 : 적 기지를 향해 일정한 속도로 이동한다.

모두 성공하였다.

 

다음은 플레이어 유닛과 적 몬스터가 일정거리 안에 들어오면 움직임을 멈추고 공격을 하도록 만들어보겠습니다.

이전에 몬스터를 소환하면서 실수를 한 것이 있었다.

Chase몬스터 대신 Move몬스터를 사용한 것인데 Move몬스터를 사용하면 메이플 스토리와 같이 움직이지만 우리가 원하는 것은 플레이어 기지를 향해 움직이는 것이었다.

Chase몬스터를 사용하면 플레이어를 추적하여 따라가게 할 수 있다. 이 때 플레이어 칸에 플레이어 기지를 넣어두면 플레이어 기지를 향해 움직이게 할 수 있다.

주황 버섯을 Chase몬스터로 다시 만들었고 AIChaseComponent의 TargetEntityRef에 플레이어 기지를 넣어두었다.

(옆의 원 2개가 중첩된 아이콘을 선택하면 Hierarchy창에 있는 오브젝트를 선택할 수 있다.)

 

적 기지와 플레이어 기지의 x값의 차이가 13.8정도로 나와서 DetectionRange(플레이어 탐지 거리)는 15로 설정하였다.

 

EnemySpawn 함수에서 몬스터 모델ID를 방금 만든 Chase몬스터 ID로 바꿔준다.

플레이어를 추적하는 것이 월드 포지션이 아닌 로컬 포지션으로 돌아가고 있어서 적 기지를 부모로 한다면 플레이어 기지까지 제대로 오지 않는 문제가 생겨 parent를 적 기지가 아닌 맵으로 지정해주었다.

소환 포지션에는 적 기지의 x좌표인 6.7을 넣어주었다.

 

 

소환 된 주황 버섯 10마리 모두 플레이어 기지까지 제대로 움직였다.

 

시간마다 다른 몬스터를 소환하기 위해 시간을 재는 스크립트가 필요하다.

스크린샷의 상단을 보면 이미 만들어둔 타이머 스크립트가 있어 이를 활용하기로 했다.

 

 

스크립트 내용은 간단하다. OnUpdate에서 delta만큼 시간을 더해주고 CheckTime을 불러와 타이머 텍스트를 업데이트 해준다.

자리수를 맞춰주기 위해 조건에 따라 00:0, 00: 등을 붙여준다.

 

하이러키 창에서 UI - DefaultGroup쪽에 text를 만들어주고 방금 만든 Timer 컴포넌트를 추가해준다.

 

 

이 후 EnemySpawn 컴포넌트에 약간의 수정이 필요하다.

해당 게임은 싱글 게임이기 때문에 서버와 연동을 할 필요가 없다. 변수들을 모두 None으로 변경한다.

Timer의 타입인 TimerRef는 ComponentRef 타입을 선택한 후 Timer 컴포넌트를 선택하면 된다.

오른쪽의 원 2개 아이콘을 선택하여 방금 만든 Timer 오브젝트를 선택한다.

 

Function쪽도 클라이언트로 변경한다.

 

EnemySpawn쪽에 조건문을 추가해준다.

방금 만든 Timer에서 playTime을 받아와 1분, 2분, 3분, 4분, 5분을 기준으로 각각 다른 몬스터를 소환한다.

모델ID부분에 해당 시간에 소환하고 싶은 몬스터ID를 넣어주면 된다.

몬스터가 5마리밖에 없어 조건문과 모델ID를 그대로 작성하였다.

몬스터가 많아지면 DataSet을 사용하여 관리를 할 것이다.

 

현재 최대 몬스터 마리수는 10마리라 1분이 지나도 새로운 몬스터가 소환되지 않아 제대로 동작 하는지 알 수 없다.

테스트용으로 EnemyState 컴포넌트를 만들어주었다.

몬스터의 maxHp는 100이며 1초마다 30의 데미지를 받는다. Hp가 0 이하로 떨어지면 죽도록 하였다.

해당 스크립트를 주황 버섯에게 적용하였다.

 

소환 된 주황 버섯은 4초 뒤 죽게 되어 최대 마리수에 도달하지 않게 되었다.

1분이 넘자 2번째 몬스터로 설정한 초록 버섯이 소환되었다.

 

스폰 기능의 목표였던

  • 최대 마리 수 : 10마리
  • 스폰 시간 : 1초에 1마리
  • 스폰 위치 : 적 기지
  • 스폰 몬스터 : 헤네시스 몬스터 5종류
  • 스폰 규칙 : 0 ~ 1분 / 1 ~ 2분 / 2 ~ 3분 / 3 ~ 4분 / 4 ~ 5분으로 구간을 나누고 각 구간마다 한 종류의 몬스터 스폰
  • 몬스터 움직임 : 적 기지에서 소환 된 몬스터는 플레이어 기지를 향해 일정한 속도로 이동한다.

를 모두 성공하였다.

1. 목표

- 메이플 스토리 월드 플랫폼을 사용하여 "팔라독"과 같은 디펜스 게임 제작하기


헤네시스를 배경으로 제작하였으며, 플레이어 기지는 '궁수 교육원' 건물을, 적 기지는 '골렘의 사원 입구'의 '스톤 골렘' 오브젝트를 사용하였습니다.

  • 최대 마리 수 : 10마리
  • 스폰 시간 : 1초에 1마리
  • 스폰 위치 : 적 기지
  • 스폰 몬스터 : 헤네시스 몬스터 5종류
  • 스폰 규칙 : 0 ~ 1분 / 1 ~ 2분 / 2 ~ 3분 / 3 ~ 4분 / 4 ~ 5분으로 구간을 나누고 각 구간마다 한 종류의 몬스터 스폰
  • 몬스터 움직임 : 적 기지에서 소환 된 몬스터는 플레이어 기지를 향해 일정한 속도로 이동한다.

 

좌측의 Preset List의 Monster 탭에서 스폰 하고 싶은 몬스터를 골라 Add to Workspace를 선택하여 MyDesk에 추가하기

 

 

MyDesk 우클릭 - Create Scripts - Create Component를 선택하여 새 컴포넌트를 생성 후 EnemySpawn로 이름 바꾸기

 

 

EnemySpawn 스크립트를 열어 EnemySpawn 함수를 추가한다.

이 때 /maps/map02/EnemyBase 이 부분은 Hierarchy 창의 적 기지를 우클릭 후 Copy Entity Path를 선택하여 붙여넣기 하면 된다.

 

MyDesk의 소환 하고자 하는 몬스터를 우클릭하여 Copy Entry ID를 선택한다.

 

작성하던 EnemySpawn 아래에 스폰 함수를 작성한다. "model://maplestorymonster$9f976b4241a64af3b9867851b8964f20" 해당 자리에 방금 복사 한 몬스터 ID를 넣어주면 된다.

 

현재 상태에서 EnemySpawn을 호출하면 적 기지 기준으로 0, 0, 0의 위치에 해당 ID의 몬스터가 "SpawnedEnemy" 이름으로 소환 된다.

Function에 OnBeginPlay를 추가하고 EnemySpawn 함수를 불러준다.

해당 스크립트에 있는 함수를 부르기 위해서는 self:을 붙여주어야 한다.

해당 스크립트에 있는 변수를 사용하기 위해서는 self.을 붙여주어야 한다.

 

Hierarchy창의 common에 EnemySpawn 스크립트를 붙여준 후 게임을 실행해보면

 

적 기지에서 주황 버섯 한마리가 떨어지는 것을 볼 수 있다.

하지만 우리가 아는 주황 버섯과는 다른 움직임을 하고 있는데 이는 아까 Preset List에서 몬스터를 추가할 때 Chase Monster를 추가하여 그런 것이다.

(몬스터를 추가할 때 Chase Monster가 아닌 MoveMonster를 추가하였다면 우리가 아는 주황버섯 처럼 좌로 이동, 방향 틀기, 우로 이동을 반복한다.)

 

똑같은 주황 버섯이 여러 종류가 있는 모습 1220 주황버섯이 Move Moster이다.

 

다음은 최대 마리수와 스폰 시간을 설정하기 위해 변수를 추가해준다.

 

 

EnemySpawn 함수에 다음 내용을 추가한다.

self.MonsterArray앞에 #이 붙으면 해당 테이블의 현재 원소 수를 반환한다.

스폰 한 몬스터 정보를 self.MonsterArray[nextNum]에 넣어준다.

MonsterArray의 현재 원소 수를 이용하여 맵에 소환 된 몬스터의 마리 수를 관리하는 것이다.

 

EnemySpawn 스크립트에 새로운 함수 GetCurMonsterCount를 만들어준다.

해당 함수는 현재 몬스터가 몇마리 존재하는지 알려주는 함수이다.

i = 1부터 시작하여 #self.MonsterArray 즉, MonsterArray테이블의 크기 만큼 순회한다.

해당 인덱스의 몬스터가 유효한 상태인지 isvalid를 사용하여 검사한 후 i에 1을 더해 다음 인덱스를 검사한다.

유효하지 않다면 해당 인덱스를 remove해준다.

테이블의 모든 원소를 순회하여 유효하지 않은 원소를 삭제한 후 MonsterArray의 크기를 반환한다.

 

EnemySpawn 에 OnUpdate를 추가한 후

self.Time = self.Time + delta를 통해 누적 시간을 구해준다.

누적 시간이 1초가 넘는다면 시간을 초기화 해준 후 방금 만든 함수인 GetCurMonsterCount를 사용하여 현재 몬스터 수를 구해준다.

null 검사를 통해 이상이 있다면 return을 해준다. (루아에서는 nil을 사용하여 null을 표현한다.)

현재 몬스터 수가 우리가 정한 최대 몬스터 수보다 적다면 EnemySpawn 함수를 실행하여 몬스터를 소환한다.

 

OnBeginPlay에는 self.MonsterArray = {}를 통해 MonsterArray  테이블을 빈 테이블로 초기화 해준다.

 

게임을 플레이하여 확인해보면 1초마다 적 기지에서 주황 버섯이 소환 된다.

아무리 기다려도 10마리 이상은 소환 되지 않는다.

 

 

여러 종류의 몬스터를 시간에 따라 소환하고, 해당 몬스터가 플레이어 기지를 향해 일정한 속도로 이동하는 부분은 다음 글에서 이어서 쓰도록 하겠습니다.

+ Recent posts