Post

p5js로 시작하기 [4]

이 글에는 이해를 돕기 위한 거짓말이 조금 포함되어 있습니다.

이미 준비된 구조

우리는 공 튀기기를 만들면서 공 하나에 필요한 거의 대부분의 구조를 만들었습니다. 그 구조를 개량하고 class로 만들어서 관리하면 될 것 같네요 👍
일단 필요한 정보들을 추려서 class를 만들어봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function setup() {
  createCanvas(400, 400);
  ball = new Ball(30, 30)
}

var ball;

function draw() {
  background(220);
  ball.update();
}

class Ball {
  // 공마다 시작 위치가 달라야하니 위치는 직접 받습니다.
  constructor(_pos_x, _pos_y) {
    // javascript 에서는 class 내에 별도 선언 없이 this.*를 사용하여 실시간 선언
    this.pos_x = _pos_x;
    // 이 클래스의 pos_y에, 밖에서 받아온 _pos_y를 사용하겠다
    this.pos_y = _pos_y;
    this.speed_x = 5;
    this.speed_y = 2;
    this.acc_y = 1;
    this.direction_x = 1;
    this.direction_y = 1;
  }
  
  update() {
    this.pos_x += this.speed_x * this.direction_x;
    this.pos_y += this.speed_y * this.direction_y;
    
    if (this.pos_x + 10 > width || this.pos_x < 10) {
      this.direction_x *= -1;
    }
    if (this.pos_y + 10 > height) {
      this.acc_y *= -1.2;
    }
    if (this.pos_y + 10 > height || this.pos_y < 10) {
      this.direction_y *= -1;
    }
    if (this.pos_y + 10 > height) {
      this.pos_y = height - 10;
    }
    this.speed_y += this.acc_y;
    circle(this.pos_x, this.pos_y, 20);
  }
}

클래스 안에 update() 함수는 클래스에는 매 프레임마다 실행되는 함수가 없어서 draw() 함수에서 매 프레임마다 실행시키려고 만든겁니다.

더 많은 공

이제 우리는 공에 대한 구조를 가지고 있으니, 더 우아하게 공을 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setup() {
  createCanvas(400, 400);
  ball = new Ball(30, 30)
+  ball2 = new Ball(45, 60);
}

var ball;
+var ball2;

function draw() {
  background(220);
  ball.update();
+  ball2.update();
}

단 세줄의 코드를 추가해서 말이죠!

더 더 더 많은 공

근데 이런 식이면, 공을 만들 때 마다 코드가 3줄씩 늘어나는 작은 비극이 있습니다. 지금은 공이 2개니 별 문제 없지만, 공이 100개, 1000개 필요할 경우 관리가 복잡해질 것 같네요.

이 부분에서 우리는 코딩의 꽃인 반복문을 사용해볼 수 있습니다.

반복하는게 아니고 공을 좀 더 만드는 것 뿐인데

조금 다르게 생각할 필요가 있습니다. 그냥 여러개를 만드는게 아니라 공 만들기를 여러번 반복한다라구요.


for

개체 생성 반복하기

대표적인 반복문으로 for()문이 있습니다.

기본 모양새는 이렇게 되어있습니다.

1
2
3
4
5
// 기본 구성
for (let i = 0; i < 100; i++) {
  // 반복 행동 코드 블럭
  console.log(i, '번째 반복 시행.');
}

이렇게 구성되어 있구요,

1
2
3
for (<변수 선언>; <조건문>; <변형>) {
  <코드 블럭>
}

이 순서로 동작합니다.

1
2
3
for ( (0) ; (1) ; (3) ) {
  (2)
}

구성 따라가기

위 예시에 있는 for()문을 따라가면

  1. 변수 i를 선언하고, i = 0 으로 설정합니다.
  2. i(0) < 100 인가요? - true
  3. 반복 행동 코드 블럭을 실행하여 0번 반복 시행. 을 콘솔에 출력합니다.
  4. i++ 처리 해줍니다. (이제 i = 1)
  5. i(1) < 100 인가요? - true
  6. 반복 행동 코드 블럭을 실행하여 1번 반복 시행. 을 콘솔에 출력합니다.
  7. i++ 처리 해줍니다. (이제 i = 2)
  8. i(99) < 100 인가요? - true
  9. 반복 행동 코드 블럭을 실행하여 99번 반복 시행. 을 콘솔에 출력합니다.
  10. i++ 처리 해줍니다. (이제 i = 100)
  11. i(100) < 100 인가요? - false
  12. 반복문 이후 코드를 실행합니다

이런식으로, 변수 선언은 시작할 때 한번만 진행한 후 조건이 거짓이 될 때까지 코드 블럭을 반복시행합니다.
이 때, 코드 블럭에서 반복하는 행동을 이번 프레임 안에 전부 실행합니다. 이러한 특성으로 인해, 1프레임(보통 1/60초) 안에 반복문이 해결되지 않으면 프로그램이 멈추거나 화면이 두둑두둑 끊기는 현상을 마주할 수 있습니다.

코드 블럭과 지역 변수

함수는 무엇인가 글에서 코드 블럭 얘기를 하며 코드의 지역 구분이라는 표현을 썼었습니다. 지금이 설명할 적기인 것 같군요.

우리가 중괄호 {}를 사용하여 코드를 작성하면, 중괄호 안쪽에는 일종의 지역이 형성됩니다.

1
2
3
4
5
6
7
8
9
10
var TargetNumber = 20;
let TestThis = 'Test';

function TestThis() { // <-- 이 지역은 함수
  if (TargetNumber < 20) { // <-- 이 지역은 조건문 발동 부분
    let ThisNumber = 10;
    var Variable = 'Text';
    console.log('숫자가 작습니다');
  }
}

지역 내에도 변수를 만들 수 있는데 이런 경우 지역 변수라고 부르며 그 지역을 벗어나는 경우 변수가 삭제됩니다. 자바스크립트의 경우 변수 선언하는 방식이 varlet 두가지가 있는데

var: 지역과 무관하게 사용할 수 있음 (전역 변수)
let: 해당 지역을 벗어나면 사용할 수 없음 (지역 변수)

  • 둘 다 변수 종류에 자유로움

이렇게 동작합니다.
이제 다시 반복문 코드를 보면

1
2
3
4
5
6
// for()문의 선언부에서 생성된 i 는
for (let i = 0; i < 100; i++) {
  // 이 코드 블럭을 반복할 때까지만 사용 가능하고
  console.log(i, '번째 반복 시행.');
}
// 코드 블럭을 벗어나면 사용할 수 없습니다. (선언되지 않은 변수라고 오류 발생)

array

반복문으로 변수 관리하기

이제 반복문을 쓸 수 있게 되었으니, 여러 변수를 한번에 다루는 방법만 알아내면 되는데요. 아쉽게도 지금과 같은 방법으로는 변수를 한번에 관리할 수 없습니다.

1
2
3
4
5
var ball_0;
var ball_1;
var ball_2;

...

이런 식으로 변수를 생성하고 for()문을 사용하면 되지 않나요?

네! 개념적으로 완벽하게 접근하셨는데 ball_n의 방식으로 변수를 불러오는 것은 컴퓨터 구조상 어렵습니다. 대신 그 작업에 적합한 변수 종류가 있습니다.

배열(Array)

var_types

우리는 편하게 쓰고 있지만 사실 변수에는 문자, 숫자 등의 다양한 종류가 있다고 했죠?
그러한 종류 중 하나로 배열이 있습니다. 배열은 변수가 적힌 리스트 종류로, 그림에 있는 체크리스트 판 자체가 이에 해당합니다.

1
var Array = [];

코드에서는 이런 식으로 생성할 수 있습니다.

배열의 경우, 자료를 구분/관리하는 첫 칸이 숫자로 되어있어 줄 세워서 일괄 처리하기에 용이합니다. 정보는 예시 이미지처럼 포인터(램 주소)가 될 수도 있지만, 바로 담아낼만큼 작은 정보라면 직접 작성되기도 합니다.

또한 배열 변수는 변수가 적힌 리스트 그 자체가 아니고, 변수가 적힌 리스트가 배정받은 포인터를 저장합니다. 이것에 대해서는 다른 글에서 다룰께요.

리스트에 정보 추가하기

이러한 리스트에 정보를 작성하려면 변수명[첫 칸] = 값이라서 작성하시면 됩니다. 컴퓨터의 고질적 구조 덕분에 숫자를 0부터 세는 녀석이긴 하지만, 상식은 없어도 일 잘하는 녀석이니까, 그 정도는 우리가 맞춰줍시다.

우리가 만든 공을 저 리스트에 담아 관리하려면 대충 이런 모양새가 되어야겠네요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setup() {
  createCanvas(400, 400);
-  ball = new Ball(30, 30);
+  ball[0] = new Ball(30, 30);
}

-var ball;
+var ball = [];

function draw() {
  background(220);
-  ball.update();
+  ball[0].update();
}

...

아, 리스트 0번에 입력한 공이 평소처럼 잘 튀깁니다.

드디어, 지금이야 말로 더 더 많은 공 !

이제 앞서 사용해본 for()문과 합쳐서 만들어봅시다.
일단 배열에는 push()라고 하는, 배열 맨 뒤에 값을 추가하기 함수가 있습니다. 리스트가 막 생성된 빈 리스트라면 0번 리스트를 추가하고, 0번에 값이 있으면 1번에 값을 추가하는 식이에요.

이 기능을 이용해서 공을 만들려면

1
2
3
4
5
6
7
8
function setup() {
  createCanvas(400, 400);
-  ball[0] = new Ball(30, 30);
+  for(let i = 0; i < 100; i++) {
+    let newBall = new Ball(30, 30);
+    ball.push(newBall);
+  }
}

이런 식으로 반복 생성을 할 수 있고, 이 모든 공들은 업데이트가 되어야하니

1
2
3
4
5
6
7
function draw() {
  background(220);
-  ball[0].update();
+  for (let i = 0; i < ball.length; i++) {
+    ball[i].update();
+  }
}

이런 식으로 전부 업데이트 해줘야겠습니다. length 변수는 숫자 리스트 타입에 내장된 변수로, 현재 리스트에 몇줄의 정보가 적혀있는지를 알려줍니다. 우리의 경우, 내용을 100개를 만들었으니 100을 돌려줍니다.

공이 하나뿐이 없어요

100개의 공이 전부 같은 위치에서 시작하여, 같은 방식으로 튀기기 때문에 완벽하게 겹쳐보이는 것입니다. random() 함수를 사용하여 임의의 숫자를 마구잡이 만들어서 공을 만들 때 대입해봅시다.

1
2
3
4
5
6
7
8
9
10
function setup() {
  createCanvas(400, 400);
  for(let i = 0; i < 100; i++) {
-    let newBall = new Ball(30, 30);
+    let random_x = random(width);
+    let random_y = random(height / 2);
+    let newBall = new Ball(random_x, random_y);
    ball.push(newBall);
  }
}

완성!
finished


이제 공끼리 서로 튀기게 해주세요

아뇨, 우리는 거기까지 진행하지는 않을 겁니다.
부담없이 코딩을 시작할 수 있는 구성의 일부로서 진행한 것일 뿐입니다. 지금까지 작성된 코드로 공끼리 튀기기를 구현하려면 생각보다 많은게 다시 작성되어야 합니다. (이 짧은 코드에서 재작성이 많이 된다니.. ! )

물론 class 구조를 좀 더 연습하고 싶다면 개인적으로 진행해보는 것을 추천합니다. 코딩을 잘한다는 것은 코드를 얼마나 잘 쓰는지가 아니라, 코드를 얼마나 잘 구성하는지이기 때문에, 별 것 아닌 것 같다고 생각되는 것도 직접 코딩하며 경험을 쌓는 것은 큰 도움이 됩니다.

농담같았던 게임엔진 사용

이 곳에서는 고도엔진을 다룰 예정입니다. 이 글을 작성하는 주인장은 고도엔진 4버전의 훌륭한 도약에 매우 예의주시 중입니다.
이에 대해 관심이 있다면 고도엔진 뉴스 페이지에서 정보를 둘러보세요 😄

이럴거면 공을 굳이 왜 튀긴거죠?

우리가 연습삼아 진행했던 공 튀기기를 게임엔진에서 만들면 엄청 빠른 속도로 구성할 수 있습니다. 그냥 벽 개체, 공 개체, 약간의 개체 설정을 해두면 끝납니다. 게임 엔진에서는 개체의 질량, 중력, 마찰력 등 수많은 물리 연산 덩어리를 제공해주니까요. 그리고 그러한 연산 덩어리는, 우리가 직접 코딩한 것처럼, 누군가 정성스럽게 코딩한 것입니다.
우리는 그것을 이해하기 위해 연습한 겁니다. 그 모든 것들이 누군가 만든 코드지, 컴퓨터가 자동으로 제공해준게 아니란 것을 몸소 체험한 겁니다.

그러니까 기억하세요.
원하는 동작하는 방식을 본인이 명확히 이해하고 있다면, 무엇이든 만들 수 있습니다.
직접 코딩하는 경험이 많아질수록, 어디까지든 점점 더 명확해질 겁니다.

This post is licensed under CC BY 4.0 by the author.