p5.js로 헤엄치는 고래 만들기(2)
프로젝트 소개
지난 포스트 에 이어서 오늘은 제일 복잡했던 고래의 움직임에 대해 글을 쓰려고 한다.
👉🏻 소스 코드
고래의 움직임
(1) whale.ts
1. 고래위치
1
this.wPos.add(this.speedX, this.speedY);
위치 = 위치 + 속도
- wPos는 고래의 현재 위치를 나타내며, speedX와 speedY는 고래의 속도를 나타낸다.
- add 메서드는 고래의 현재 위치에 속도를 더하여 새로운 위치를 계산.
2. 벽과의 충돌
고래가 화면의 경계(boundary)를 넘어가면 speedX speedY 의 부호를 바꿔서 방향을 반전한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 벽과의 충돌 체크
if (
this.wPos.x < this.boundary ||
this.wPos.x > window.innerWidth - this.boundary
) {
this.speedX *= -1; // x축 방향 반전
}
if (
this.wPos.y < this.boundary ||
this.wPos.y > window.innerHeight - this.boundary
) {
this.speedY *= -1; // y축 방향 반전
}
3. 몸통
1
2
3
4
5
6
7
8
this.fins.forEach((f, i) => {
if (i > 0) {
const prevFin = this.fins[i - 1];
f.update(prevFin.pos.x, prevFin.pos.y, i === this.numFin - 1, i);
} else {
this.fins[0].update(this.wPos.x, this.wPos.y);
}
});
- fins 배열을 순회하여 각 지느러미를 업데이트한다.
- 머리(첫번째 몸통)는 고래의 현재 위치에 따라 업데이트하고, 나머지 몸통은 이전 몸통의 위치를 참조하여 업데이트한다.
- 꼬리(마지막 몸통)는 사다리꼴이 아닌 삼각형으로 나타내야 하므로 따로 조건을 주었다. (i === this.numFin - 1).
(2) fin.ts
작명이 잘못된건 인정.. 지느러미라고 쓰고 몸통이라고 읽겠다.
- 고래의 형태를 그리고, 이를 움직이는 물체의 위치와 방향에 맞게 조정하는 과정을 설명하고자한다. 이를 통해 각 몸통 부분을 화면에 그리면서 실제로 움직이는 물체의 위치와 방향에 적절하게 반영하였다. 코드를 이해하기 위해 각 단계별로 작성한 이유와 고민했던 부분을 자세히 살펴보겠다. 👉🏻 Koi flock: p5.js 이 코드를 많이 참고 하였다.
고민:
몸통의 길이와 방향을 계산하여, 움직이는 위치와 방향으로 그려질 수 있도록 해야했다. 몸통이 향해야 할 목표 위치(target)와 현재 위치(this.pos) 간의 벡터를 계산하여 사다리꼴의 모양과 방향을 계산하는것이 어려웠다.
구현
1. 각도 계산 및 위치 업데이트
- angle은 몸통의 끝부분이 현재 위치에서 얼마나 떨어져 있는지를 결정하는 데 필요하다. 지느러미의 높이와 방향에 따라 끝부분의 위치를 정확하게 계산하려면 이 각도가 필요하다.
1
2
3
4
5
6
7
8
9
10
// 현재 움직이는 위치와 fin위치간 거리의 차이를 구한다.
const target = this.p5.createVector(x, y);
const dir = p5.Vector.sub(target, this.pos);
dir.setMag(isTail ? this.w - 30 : this.w);
dir.mult(-1);
// 현재 움직이는 위치가 어디로 향하는지, 각도를 구한다.
this.angle = dir.heading();
this.pos = p5.Vector.add(target, dir);
2. 몸통 그리기
(1) 삼각함수를 사용한 위치 계산
- 지느러미의 높이(h)를 설정하고, 각도에 따른 이동 벡터를 계산하여 arg2에 위치를 설정
- h: 지느러미의 높이, 즉 지느러미가 실제로 차지하는 공간의 크기
- arg2: 현재 위치(this.pos)와 지느러미의 방향(angle) 및 길이(h)를 기반으로 지느러미의 끝부분 위치 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 사다리꼴의 기본 형태 정의
let orig = this.p5.createVector(0, 0);
let other = this.p5.createVector(
0,
-this.p5.dist(this.pos.x, this.pos.y, arg2.x, arg2.y) // 아래 방향 기준 사다리꼴의 길이
);
const h = isTail ? 20 : 50;
const arg2 = this.p5.createVector(0, 0);
const dx = h * this.p5.cos(this.angle);
const dy = h * this.p5.sin(this.angle);
arg2.set(this.pos.x + dx, this.pos.y + dy);
cos와 sin 함수를 사용하여 각도(this.angle)에 따른 이동 벡터를 계산한다. 각도에 따라 이동 벡터를 조정하면 지느러미가 실제로 회전하고 움직이는 방향에 맞게 위치를 조정할 수 있다.
벡터를 x축과 y축 방향으로 분해할 때, 삼각함수는 다음과 같은 역할을 한다:
- 코사인 함수 (cos): 주어진 각도에서 x축 방향으로의 거리를 계산 ```ts const dx = h * this.p5.cos(this.angle);
// 현재 위치에서 몸통의 끝부분이 x축 방향으로 이동해야 하는 거리 // : 각도에 따라 코사인 값은 x축 방향으로의 이동량을 제공
1
2
3
4
5
- 사인 함수 (sin): 주어진 각도에서 y축 방향으로의 거리를 계산
```ts
const dy = h * this.p5.sin(this.angle);
// 현재 위치에서 지느러미의 끝부분이 y축 방향으로 이동해야 하는 거리
// 각도에 따라 사인 값은 y축 방향으로의 이동량을 제공
- arg2.set에서 현재 위치(this.pos)와 지느러미의 방향(angle) 및 길이(h)를 기반으로 지느러미의 끝부분 위치 설정한다.
(2) 사다리꼴의 네점 계산
1
2
3
4
5
6
7
8
9
10
11
12
13
let p1, p2, p3, p4;
p1 = orig.copy(); // 왼쪽 위
p2 = orig.copy(); // 오른쪽 위
p3 = other.copy(); // 오른쪽 아래
p4 = other.copy(); // 왼쪽 아래
p1.add(-widthFront / 2, margin);
p2.add(widthFront / 2, margin);
p3.add(widthBack / 2, -margin);
p4.add(-widthBack / 2, -margin);
// 사다리꼴의 네 점을 배열로 저장
const trapPoints = [p1, p2, p3, p4];
(3) 회전 및 위치 조정
- 사다리꼴의 각 점을 회전시켜 올바른 방향으로 조정. 이후 위치를 this.pos로 이동시킨다. => (0,0) 임의 값을 기준으로 점이 계산되었으므로 실제 위치값을 넣어준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (let i = 0; i < trapPoints.length; i++) {
let h = this.pos.copy();
// h 벡터는 현재 위치 this.pos와 이동된 위치 arg2 사이의 벡터.두 위치 간의 방향.
h.sub(arg2);
// 사다리꼴의 각 점을 h 벡터의 방향으로 회전
trapPoints[i].rotate(h.heading());
// 사다리꼴을 90도 회전. 사다리꼴이 h 벡터와 수직으로 배치되도록 하기 위함
trapPoints[i].rotate(this.p5.radians(90));
// 회전된 사다리꼴의 각 점을 원래 위치 this.pos로 이동
trapPoints[i].add(this.pos);
}
this.p5.fill(255, 200);
this.p5.noStroke();
(4) 사다리꼴 그리기
- fill, noStroke, curveTightness 등을 설정한 후, 사다리꼴의 형태를 그렸다. curveVertex를 사용하여 곡선 형태로 사다리꼴을 그릴 수 있다. 꼬리의 경우 trapPoints를 3개만 주면된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
this.p5.fill(255, 200);
this.p5.noStroke();
// curveTightness 설정
this.p5.curveTightness(0.5);
// 둥근 사다리꼴 그리기
this.p5.beginShape();
for (let i = 0; i < trapPoints.length; i++) {
this.p5.curveVertex(trapPoints[i].x, trapPoints[i].y);
}
// 마지막 점은 시작점을 위해 반복
this.p5.curveVertex(trapPoints[0].x, trapPoints[0].y);
this.p5.curveVertex(trapPoints[1].x, trapPoints[1].y);
this.p5.endShape(this.p5.CLOSE);
👉🏻 최종 코드는 여기를 확인해주세용 :)
추가적으로 개발이 필요한 것
- 머리를 기준으로 몸통과 꼬리가 sine 운동을 한다.
- 몸통의 무늬는 머리에서 꼬리로 갈 수록 점(dot)의 밀도가 낮아진다.
- 양 옆의 지느러미는 움직이는 방향을 기준으로 상하 운동을 한다.
- 전시하였을때 모두가 고래를 터치하는 행동을 보였다. 고래를 터치하였을 때 꿈틀거리거나 말풍선을 띄워 대화하는것처럼 보이는 효과를 추가해야겠다.
참고자료
This post is licensed under CC BY 4.0 by the author.