pmjuu / climick-client

πŸ§— A game where you can click on the player's hands and feet to climb

Home Page:https://climick.netlify.app/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Climick

" Climbing + Click "

πŸ§— 마우슀둜 μ†λ°œμ„ ν΄λ¦­ν•΄μ„œ 클라이밍을 ν•  수 μžˆλŠ” κ²Œμž„μž…λ‹ˆλ‹€.

배포된 μ‚¬μ΄νŠΈ


Table of Contents


Preview

start


Motivation

클라이밍, μˆ˜ν•™, 물리 - μ œκ°€ μ’‹μ•„ν•˜λŠ” μš”μ†Œλ“€μ„ λͺ¨μ•„μ„œ 아이디어λ₯Ό κΈ°νšν–ˆμŠ΅λ‹ˆλ‹€.

λ‹¨μˆœνžˆ νŠΉμ • 라이브러리 μ‚¬μš©λ²•μ„ μ•„λŠ” 것을 λ– λ‚˜μ„œ, λͺ¨λ‘κ°€ 배운 μˆ˜ν•™,물리 곡식을 λ°”νƒ•μœΌλ‘œ λ…Όλ¦¬μ μœΌλ‘œ 사고λ₯Ό μ „κ°œν•΄λ‚˜κ°€λŠ” 과정을 보여주고 μ‹Άμ—ˆμŠ΅λ‹ˆλ‹€. μ‚¬λžŒμ˜ κ΄€μ ˆ μ›€μ§μž„ 및 물리엔진 κ΅¬ν˜„μ΄λΌλŠ” μƒμ†Œν•œ λ¬Έμ œμ— 도전해보고 μ‹Άμ—ˆμŠ΅λ‹ˆλ‹€.

클라이밍짐에 가지 μ•Šλ”λΌλ„ 클라이밍 λ™μž‘λ“€μ„ μ—°μŠ΅ν•  수 μžˆλŠ” μ„œλΉ„μŠ€κ°€ 있으면 μ’‹κ² λ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.
κ·Έλž˜μ„œ 이 κ²Œμž„μ€ λ‹¨μˆœνžˆ 재미둜만 ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ 배울 점이 μžˆλ„λ‘ κΈ°νšν–ˆμŠ΅λ‹ˆλ‹€.

  1. ν•˜μ²΄ μ›€μ§μž„μ„ μƒκ°ν•˜λŠ” μŠ΅κ΄€ κΈ°λ₯΄κΈ°
    • μ‹€μ œλ‘œ λ¬΄κ²Œμ€‘μ‹¬μ„ 올릴 λ•ŒλŠ” νŒ”λ‘œλ§Œ λŒμ–΄μ˜¬λ¦¬λŠ”κ²Œ μ•„λ‹ˆλΌ, λ°œμ„ λ¨Όμ € 올리고 ν•˜μ²΄νž˜μœΌλ‘œ λ¬΄κ²Œμ€‘μ‹¬μ„ μ˜¬λ¦¬λŠ”κ²Œ 체λ ₯ μ†Œλͺ¨ νš¨μœ¨μ— μžˆμ–΄μ„œ μ’‹μŠ΅λ‹ˆλ‹€.
  2. 루트 νŒŒμΈλ”© (route finding) μ—°μŠ΅ν•˜κΈ°
    • 루트 νŒŒμΈλ”©: ν™€λ“œλ₯Ό λ³΄λ©΄μ„œ μ–΄λ–€ λ™μž‘κ³Ό μˆœμ„œλ‘œ ν™€λ“œλ₯Ό μž‘μ„μ§€ νŒλ‹¨ν•˜λŠ” 것

Challenges

1. κ΄€μ ˆ μ›€μ§μž„ ν‘œν˜„ν•˜κΈ°

μ–΄λ–»κ²Œ ν•΄μ•Ό νŒ”(닀리)κ°€ 손(발) μ›€μ§μž„μ— 따라 μ ‘νžκΉŒ?

각 신체 λΆ€μœ„λ₯Ό ν•˜λ‚˜μ˜ new Graphics() 객체둜 μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€.

// src/utils/player.js

export const body = new Graphics();
const leftUpperArm = new Graphics();
const leftForeArm = new Graphics();
const leftHand = new Graphics();
const rightUpperArm = new Graphics();
const rightForeArm = new Graphics();
const rightHand = new Graphics();
const leftThigh = new Graphics();
const leftCalf = new Graphics();
const leftFoot = new Graphics();
const rightThigh = new Graphics();
const rightCalf = new Graphics();
const rightFoot = new Graphics();

* 상완(upperArm): μ–΄κΉ¨μ—μ„œ νŒ”κΏˆμΉ˜κΉŒμ§€μ˜ νŒ” 일뢀뢄을 μ§€μΉ­ν•©λ‹ˆλ‹€. * μ „μ™„(foreArm): νŒ”κΏˆμΉ˜μ—μ„œ 손λͺ©κΉŒμ§€μ˜ νŒ” 일뢀뢄을 μ§€μΉ­ν•©λ‹ˆλ‹€.

νŒ”(닀리)의 μ „μ™„ 및 상완은 μ‹œμž‘μ  μ’Œν‘œμ™€ x,y λ³€ν™”λŸ‰(upperArmDxy)으둜 그릴 수 μžˆλŠ” Line으둜 λ‚˜νƒ€λƒˆμŠ΅λ‹ˆλ‹€.

// src/utils/drawLimb.js

const upperArmDxy = {
  dx: limbLength * getCos(upperArmAngle) * flagX,
  dy: -limbLength * getSin(upperArmAngle) * flagY,
};

upperArm.position.set(shoulder.x + flagX * flagY, shoulder.y);

upperArm
  .lineStyle(limbWidth + 3, COLOR.SKIN)
  .lineTo(upperArmDxy.dx, upperArmDxy.dy);

drawLimb.js 전체 μ½”λ“œ

각도 및 신체 λΆ€μœ„ μ’Œν‘œ 계산 κ³Όμ •

image

손과 μ–΄κΉ¨μ˜ μ’Œν‘œλŠ” 항상 μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ μœ„ κ·Έλ¦Όμ—μ„œ theta1κ³Ό theta2λ₯Ό ꡬ할 수 μžˆμŠ΅λ‹ˆλ‹€.

// src/utils/moveJoint.js

const handToShoulder = getDistance(shoulder, cursorInContainer);
const h = Math.sqrt(limbLength ** 2 - (handToShoulder / 2) ** 2) || 0; // μ΄λ“±λ³€μ‚Όκ°ν˜• HES의 높이
const theta1 = getAngleDegrees(handToShoulder / 2, h);
const theta2 = getAngleDegrees(
  flagX * (shoulder.x - cursorInContainer.x),
  shoulder.y - cursorInContainer.y
);

νŒ”μ˜ μ „μ™„κ³Ό μƒμ™„μ˜ κΈΈμ΄λŠ” κ°™κ²Œ μ„€μ •ν•˜κ³ ,
상완이 지면과 μ΄λ£¨λŠ” 각 (theta1 - theta2)λ₯Ό κ΅¬ν•΄μ„œ νŒ”κΏˆμΉ˜(elbow) μ’Œν‘œλ₯Ό 계산할 수 μžˆμŠ΅λ‹ˆλ‹€.

const elbow = {
  x: shoulder.x - flagX - limbLength * getCos(theta1 - theta2) * flagX,
  y: shoulder.y + limbLength * getSin(theta1 - theta2),
};

νŒ”κΏˆμΉ˜(elbow) μ’Œν‘œλ₯Ό λ°”νƒ•μœΌλ‘œ 상완(upperArm)κ³Ό μ „μ™„(foreArm)을 κ·Έλ¦½λ‹ˆλ‹€.

const upperArmDxy = {
  x: elbow.x - shoulder.x,
  y: elbow.y - shoulder.y,
};

upperArm
  .lineStyle(limbWidth + 3, COLOR.SKIN)
  .lineTo(upperArmDxy.x, upperArmDxy.y);

foreArm.position.set(elbow.x, elbow.y);

foreArm
  .lineStyle(limbWidth, COLOR.SKIN)
  .lineTo(hand.x - elbow.x, hand.y - elbow.y);

πŸ”½ 손 μœ„μΉ˜μ— 따라 νŒ”μ΄ μžμ—°μŠ€λŸ½κ²Œ μ ‘νžˆλŠ” λͺ¨μŠ΅

손을 λ»—μœΌλ©΄ λ‹€λ₯Έ μ‹ μ²΄λΆ€μœ„λ„ λ”°λΌμ„œ μžμ—°μŠ€λŸ½κ²Œ μ›€μ§μ΄κ²Œ ν•  수 μžˆμ„κΉŒ?

클라이밍 λ™μž‘μ€ μ—¬λŸ¬ μ‹ μ²΄λΆ€μœ„μ˜ μ›€μ§μž„μ΄ λ³΅ν•©μ μœΌλ‘œ 이루어진 κ²°κ³Όλ¬Όμž…λ‹ˆλ‹€.
(ex. 손을 λ»—μ–΄ 멀리 μžˆλŠ” 물체λ₯Ό μž‘λŠ”λ‹€ = 손을 λ“œλž˜κ·Έν•΄μ„œ νŒ” κ΄€μ ˆμ΄ μ΄λ™ν•œλ‹€ + λͺΈν†΅μ΄ 손 λ°©ν–₯으둜 μ΄λ™ν•˜λ©΄μ„œ λ‹€λ₯Έ κ΄€μ ˆλ„ 움직인닀​)

μ²˜μŒμ—λŠ” 손을 μœ„λ‘œ λ»—κ³ , λͺΈν†΅ 올리고, 손 λ»—λŠ” 과정을 ν†΅ν•΄μ„œ λ¬΄κ²Œμ€‘μ‹¬μ„ 올리게 ν–ˆμŠ΅λ‹ˆλ‹€.
그런데 이 μ›€μ§μž„μ€ λΆ€μžμ—°μŠ€λŸ¬μ› κ³ , μ‚¬μš©μž μž…μž₯μ—μ„œ λΆˆνŽΈν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έλž˜μ„œ ν•œ 손을 λ»—λŠ” λ™μž‘μ„ ν•  λ•Œ, λ‹€μŒκ³Ό 같은 단계λ₯Ό 거쳐 μ›€μ§μž„μ„ κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.

  1. λ¨Όμ € νŒ” κ΄€μ ˆμ„ μ›€μ§μ΄λŠ” ν•¨μˆ˜ moveJoint()λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
    이 κ³Όμ •μ—μ„œ μ†μ—μ„œ μ–΄κΉ¨κΉŒμ§€ 거리가 μ œν•œλœ νŒ” 길이λ₯Ό λ„˜μ–΄μ„ λ‹€λ©΄ theta2λ₯Ό λ°˜ν™˜ν•˜κ³ ,

  2. λͺΈν†΅μ„ μ›€μ§μ΄λŠ” ν•¨μˆ˜ moveBodyTo()λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

const theta2 = moveJoint(
  ...leftArmList,
  ...armSize,
  cursorInContainer,
  1,
  1,
  handRadius
);

if (!theta2) return;

return moveBodyTo({
  x: cursorInContainer.x + armLength * 2 * getCos(theta2) + BODY.WIDTH / 2,
  y: cursorInContainer.y + armLength * 2 * getSin(theta2) + BODY.HEIGHT / 2,
});
  1. moveBodyTo()λŠ” λͺΈν†΅μ˜ μœ„μΉ˜λ₯Ό λ°”κΎΈκ³  ν•¨μˆ˜ λ‚΄λΆ€μ—μ„œ moveJointBody()ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•΄μ„œ λ‹€λ₯Έ νŒ”λ‹€λ¦¬ κ΄€μ ˆμ„ λͺΈν†΅μ˜ μœ„μΉ˜μ— 따라 μžμ—°μŠ€λŸ½κ²Œ μ›€μ§μ΄κ²Œ ν•©λ‹ˆλ‹€.

2. library 없이 직접 물리 엔진 κ΅¬ν˜„ν•˜κΈ°

library 없이 κ΅¬ν˜„ν•˜κ²Œ 된 동기
쀑λ ₯가속도에 μ˜ν•œ μ‹ μ²΄λΆ€μœ„ 등가속 μ›μš΄λ™λ§Œ 잘 ν‘œν˜„ν•˜λ©΄ 되기 λ•Œλ¬Έμ— 3rd party library 없이 직접 κ΅¬ν˜„ν•  수 μžˆκ² λ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.

물체에 항상 μž‘μš©ν•˜λŠ” 쀑λ ₯을 μ–΄λ–»κ²Œ ν‘œν˜„ν• κΉŒ?

  1. λͺ¨λ“  사물에 κ³΅ν†΅μ μœΌλ‘œ μž‘μš©ν•˜λŠ” 쀑λ ₯ ν•¨μˆ˜λ₯Ό λ§Œλ“ λ‹€. ❌
    • μž₯점
      • 신체 λΆ€μœ„ 외에 λ‹€λ₯Έ 객체에도 λ²”μš©μ μœΌλ‘œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
      • μœ μ§€λ³΄μˆ˜κ°€ μš©μ΄ν•©λ‹ˆλ‹€.
    • 문제점
      • 신체 λΆ€μœ„λŠ” λ‹€λ₯Έ λΆ€μœ„μ™€ μ—°κ²°λ˜μ–΄μžˆμœΌλ―€λ‘œ, λ‹¨μˆœνžˆ μ•„λž˜λ°©ν–₯으둜 쀑λ ₯이 μž‘μš©ν•˜λŠ” 것 이외에 μž₯λ ₯κ³Ό 같은 λ‹€λ₯Έ νž˜λ„ μž‘μš©ν•©λ‹ˆλ‹€.
      • 힘의 ν•©λ ₯을 κ΅¬ν•΄μ„œ 신체 μ›€μ§μž„μ— μ μš©ν•˜λŠ” λ‘œμ§μ„ κ΅¬ν˜„ν•˜κΈ°μ—λŠ” μ‹œκ°„μ΄ λΆ€μ‘±ν–ˆμŠ΅λ‹ˆλ‹€.
  2. Pixi.JS기반 ν”ŒλŸ¬κ·ΈμΈ ν˜•νƒœλ‘œ 인체 쀑λ ₯ μž‘μš© ν•¨μˆ˜λ₯Ό λ§Œλ“ λ‹€. βœ…
    • 쀑λ ₯은 항상 μž‘μš©ν•˜κ³  μžˆλŠ”λ°, νŠΉμ • μƒν™©μ—μ„œ 쀑λ ₯이 λ‹€λ₯Έ 힘의 ν•©λ ₯보닀 크게 μž‘μš©ν•΄μ„œ μ‹ μ²΄λΆ€μœ„κ°€ μ•„λž˜ λ°©ν–₯으둜 λ“±κ°€μ†μ›μš΄λ™ν•˜κ±°λ‚˜ ν”Œλ ˆμ΄μ–΄κ°€ μ•„λž˜λ‘œ μ΄λ™ν•œλ‹€κ³  μ „μ œν•˜κ³  λ‘œμ§μ„ κ΅¬μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

쀑λ ₯이 λ‹€λ₯Έ 힘의 ν•©λ ₯보닀 크게 μž‘μš©ν•˜λŠ” 상황 경우의 수 λ‚˜λˆ„κΈ°

1. λ“œλž˜κ·Έμ•€λ“œλ‘­ ν›„ 손(발) ν•˜λ‚˜κ°€ ν™€λ“œμ—μ„œ 떨어진닀면

νŒ”(닀리)λŠ” μ–΄κΉ¨(κ³ κ΄€μ ˆ)을 νšŒμ „μΆ•μœΌλ‘œ ν•΄μ„œ 쀑λ ₯ μž‘μš© λ°©ν–₯으둜 등가속 μ›μš΄λ™μ„ ν•©λ‹ˆλ‹€.

  • 손이 ν™€λ“œμ—μ„œ λ–¨μ–΄μ‘Œλ‹€λŠ” 것을 μ–΄λ–»κ²Œ μ•Œ 수 μžˆμ„κΉŒ?

    • ν™€λ“œ 각각의 μž…μž₯μ—μ„œ pointerdown 이벀트 λ°œμƒ 이후 canvas μ—μ„œ pointerup μ΄λ²€νŠΈκ°€ λ°œμƒν•˜λ©΄ 쀑λ ₯ μž‘μš© ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•œλ‹€.
      β‡’ 항상 ν™€λ“œ μœ„μ— μ†λ°œμ΄ 있기 λ•Œλ¬Έμ—, ν™€λ“œλŠ” pointerdown 이벀트λ₯Ό 감지할 수 μ—†μŠ΅λ‹ˆλ‹€.
    • ν™€λ“œ μœ„μΉ˜ 기반으둜 κ°μ§€ν•œλ‹€.
      손/λ°œμ—μ„œ pointerup μ΄λ²€νŠΈκ°€ λ°œμƒν•  λ•Œ, 손/발이 정해진 ν™€λ“œ μ’Œν‘œ 내에 μœ„μΉ˜ν•˜μ§€ μ•ŠμœΌλ©΄ 쀑λ ₯ μž‘μš© ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. (νŒ”/λ‹€λ¦¬λŠ” μ–΄κΉ¨/κ³ κ΄€μ ˆ μ’Œν‘œλ₯Ό νšŒμ „μΆ•μœΌλ‘œ 등가속 μ›μš΄λ™μ„ ν•œλ‹€.)
      • 이 λ•Œ, ν”Œλ ˆμ΄μ–΄μ˜ 체λ ₯(HP) μ†Œλͺ¨κ°€ 큰 κ²ƒμœΌλ‘œ κ°„μ£Όν•˜μ—¬ HPκ°€ 빨리 μ€„μ–΄λ“­λ‹ˆλ‹€.
  • 등가속 μ›μš΄λ™μ„ μ–΄λ–»κ²Œ ν‘œν˜„ν• κΉŒ?

    • gravityRotate()ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•΄μ„œ 상완 및 μ „μ™„μ˜ νšŒμ „ 각속도(angleVelocity)λ₯Ό μΌμ •ν•œ κ°€μ†λ„λ‘œ μ¦κ°€μ‹œν‚΅λ‹ˆλ‹€.
    • 쀑λ ₯은 항상 μ•„λž˜λ°©ν–₯으둜 μž‘μš©ν•˜κΈ°μ—, 지면에 수직인 μ„ κ³Ό νŒ”μ΄ μ΄λ£¨λŠ” 각도와 κ°™μ•„μ§ˆ λ•ŒκΉŒμ§€
      μƒμ™„μ˜ 각도(upperArm.angle) 및 μ „μ™„μ˜ 각도(foreArm.angle)λ₯Ό μ¦κ°€μ‹œν‚΅λ‹ˆλ‹€.
    // src/utils/gravityRotate.js
    
    const handToShoulder = getDistance(shoulder, hand);
    const h = Math.sqrt(limbLength ** 2 - (handToShoulder / 2) ** 2) || 0;
    const theta1 = getAngleDegrees(handToShoulder / 2, h);
    const upperArmOriginalAngle = getAngleDegrees(
      foreArm.y - shoulder.y,
      foreArm.x - shoulder.x
    );
    const rotatingDirection =
      upperArmOriginalAngle / Math.abs(upperArmOriginalAngle);
    
    let angleVelocity = 0;
    
    function rotateArm() {
      angleVelocity += 0.5;
    
      const isUpperArmRotating =
        Math.abs(upperArm.angle) < Math.abs(upperArmOriginalAngle);
    
      const foreArmRotatingGoal =
        Math.abs(upperArmOriginalAngle) + theta1 * 2 * rotatingDirection * flagX;
    
      const isForeArmRotating = Math.abs(foreArm.angle) < foreArmRotatingGoal;
    
      if (isUpperArmRotating) {
        upperArm.angle += angleVelocity * 0.2 * rotatingDirection;
    
        const newAngle = upperArmOriginalAngle - upperArm.angle;
    
        foreArm.x = shoulder.x + limbLength * getSin(newAngle);
        foreArm.y = shoulder.y + limbLength * getCos(newAngle);
      }
    
      if (isForeArmRotating) {
        foreArm.angle += angleVelocity * 0.2 * rotatingDirection;
      }
    
      const newAngle = foreArmRotatingGoal - Math.abs(foreArm.angle);
    
      hand.x = foreArm.x + limbLength * getSin(newAngle) * rotatingDirection;
      hand.y = foreArm.y + limbLength * getCos(newAngle);
    
      const isRotationFinished = !isUpperArmRotating && !isForeArmRotating;
    
      if (isRotationFinished) {
        return drawLimb( ... ); // νšŒμ „μ΄ λλ‚˜λ©΄ 각도가 0으둜 λ¦¬μ…‹λœ μƒˆλ‘œμš΄ νŒ”λ‹€λ¦¬λ₯Ό κ·Έλ¦½λ‹ˆλ‹€.
      }
    
      requestAnimationFrame(rotateArm);
    }
    
    rotateArm();

2. μ–‘ 손이 ν™€λ“œμ—μ„œ 떨어진닀면

λͺΈ 전체가 μ•„λž˜ λ°©ν–₯으둜 등가속 μš΄λ™μ„ ν•©λ‹ˆλ‹€.

  • ν™€λ“œ μ’Œν‘œ λ²”μœ„ μ•ˆμ— μžˆλŠ” 왼손/였λ₯Έμ†μ˜ 개수λ₯Ό λ³€μˆ˜λ‘œ μ €μž₯ν•©λ‹ˆλ‹€.
    • 각각 μ΄ˆκΈ°κ°’μ€ 1개이며, onDragEnd() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•  λ•Œλ§ˆλ‹€ 손이 ν™€λ“œ μœ„μΉ˜μ— μžˆλŠ”μ§€ νŒλ³„ν•΄μ„œ λ³€μˆ˜κ°’μ„ λ³€κ²½ν•©λ‹ˆλ‹€.
  • onDragStart() ν•¨μˆ˜ μ‹€ν–‰ μ‹œ μ†μ˜ κ°œμˆ˜κ°€ λͺ‡κ°œμΈμ§€ ν™•μΈν•˜κ³ , 0개라면 fallDown() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
  • fallDown() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜λ©΄ ν”Œλ ˆμ΄μ–΄κ°€ ν¬ν•¨λœ playerContainerκ°€ λ°”λ‹₯에 닿을 λ•ŒκΉŒμ§€
    ν•˜κ°• 속도(descentVelocity)λ₯Ό μΌμ •ν•œ κ°€μ†λ„λ‘œ μ¦κ°€μ‹œν‚€λ©΄μ„œ μ•„λž˜λ‘œ μ΄λ™ν•©λ‹ˆλ‹€.
    function fallDown(displayText) {
      let descentVelocity = 0;
    
      function descend() {
        descentVelocity += 0.4;
        playerContainer.y += descentVelocity * 0.3;
    
        const isPlayerAboveGround =
          playerContainer.y <
          containerPosition.y -
            leftShoulder.y + (initialContainerHeight - playerContainer.height);
    
        if (!isPlayerAboveGround) {
          gameStatus.fail = true;
          holdContainer.addChild(getResultText(displayText));
          return;
        }
    
        requestAnimationFrame(descend);
      }
    
      descend();
    }

3. λ¬΄κ²Œμ€‘μ‹¬μ˜ xμ’Œν‘œκ°€ μ–‘ 발 사이에 μ—†μœΌλ©΄

ν•œμͺ½ νŒ”μ΄ 펴질 λ•ŒκΉŒμ§€ λ¬΄κ²Œμ€‘μ‹¬μ΄ 쀑λ ₯을 λ°›μ•„μ„œ μ•„λž˜λ‘œ λ‚΄λ €κ°‘λ‹ˆλ‹€.

  • λ“œλž˜κ·Έκ°€ λλ‚˜κ³  onDragEnd() ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜λ©΄, checkGravityCenter() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•΄μ„œ λ¬΄κ²Œμ€‘μ‹¬ xμ’Œν‘œκ°€ μ–‘ 발의 xμ’Œν‘œ 사이에 μžˆλŠ”μ§€ νŒλ³„ν•©λ‹ˆλ‹€.
  • λ¬΄κ²Œμ€‘μ‹¬ xμ’Œν‘œκ°€ μ–‘ 발 사이에 μ—†μœΌλ©΄ descendByGravity() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•΄μ„œ λͺΈν†΅μ΄ μ•„λž˜λ‘œ λ‚΄λ €κ°‘λ‹ˆλ‹€.
function checkGravityCenter() {
  const gravityCenterX = body.x + BODY.WIDTH / 2;

  attachedStatus.isStable =
    leftFoot.x < gravityCenterX && gravityCenterX < rightFoot.x;

  if (!attachedStatus.isStable) {
    descendByGravity();
  }

  function descendByGravity() { ... }
}
  • 이 λ•Œ, ν”Œλ ˆμ΄μ–΄μ˜ 체λ ₯(HP) μ†Œλͺ¨κ°€ 큰 κ²ƒμœΌλ‘œ κ°„μ£Όν•˜μ—¬ HPκ°€ 빨리 μ€„μ–΄λ“­λ‹ˆλ‹€.

3. UX κ°œμ„ ν•˜κΈ°

마우슀 이동 속도가 λΉ λ₯Ό λ•Œ, λΆ€λ“œλŸ¬μš΄ λ“œλž˜κ·Έ 효과λ₯Ό μ–΄λ–»κ²Œ λ‚˜νƒ€λ‚ΌκΉŒ?

* hand: 손/λ°œμ„ μ§€μΉ­ν•©λ‹ˆλ‹€.

κΈ°μ‘΄ μ½”λ“œ

  • hand 객체에 pointermove 이벀트λ₯Ό λ“±λ‘ν•΄μ„œ μ»€μ„œλ‘œ 손을 λ“œλž˜κ·Έν•  λ•Œ onDragging()ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
  • onDragging() ν•¨μˆ˜μ—μ„œ moveJoint() ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜κ³ ,
    moveJoint() ν•¨μˆ˜μ—μ„œ hand x,y μ’Œν‘œμ— μ»€μ„œμ˜ x,y μ’Œν‘œλ₯Ό λŒ€μž…ν•¨μœΌλ‘œμ¨ handλ₯Ό μ΄λ™μ‹œν‚΅λ‹ˆλ‹€.

문제점

  • 손 이동 속도가 마우슀 λ“œλž˜κ·Έ 속도λ₯Ό λͺ» λ”°λΌκ°€μ„œ λ“œλž˜κ·Έ 쀑에 λ™μž‘μ΄ λŠκΈ°λŠ” κ²½μš°κ°€ μžˆμ—ˆκ³  μ‚¬μš©μž κ²½ν—˜μ΄ μ €ν•˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

원인

  • μ»€μ„œ 이동 속도가 λΉ λ₯Ό λ•Œ, λ“œλž˜κ·Έ 쀑에 μ‹€μ‹œκ°„μœΌλ‘œ hand x,y μ’Œν‘œκ°€ μ»€μ„œμ˜ x,y μ’Œν‘œλ‘œ μ—…λ°μ΄νŠΈλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.
  • λ”°λΌμ„œ, μ»€μ„œμ˜ μ’Œν‘œκ°€ hand μ’Œν‘œλ₯Ό λ²—μ–΄λ‚˜λŠ” ν˜„μƒμ΄ λΉˆλ²ˆν•˜κ²Œ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

  • addEventListener("pointermove", onDragging) 이벀트λ₯Ό hand 객체가 μ•„λ‹ˆλΌ 뒷배경인 벽을 λ‚˜νƒ€λ‚΄λŠ” document.querySelector(".wall") 에 λ“±λ‘ν•©λ‹ˆλ‹€.
    const wall = document.querySelector(".wall");
    wall.addEventListener("pointermove", onDragging);
  • μ»€μ„œ μœ„μΉ˜κ°€ hand μ’Œν‘œ λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ”λΌλ„ (=μ–΄κΉ¨μ—μ„œ μ»€μ„œκΉŒμ§€μ˜ 거리가 νŒ” 길이(limbLength * 2)보닀 길어지더라도)
    어깨→손 벑터가 μ–΄κΉ¨β†’μ»€μ„œ 벑터와 λ°©ν–₯만 κ°™κ³  ν¬κΈ°λŠ” νŒ” 길이둜 μΌμ •ν•˜λ„λ‘ hand μ’Œν‘œλ₯Ό μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.
    // src/utils/moveJoint.js
    
    const cursorToShoulder = getDistance(shoulder, cursorInContainer);
    ...
    if (cursorToShoulder > limbLength * 2) {
      hand.x = shoulder.x - limbLength * 2 * getCos(theta2) * flagX;
      hand.y = shoulder.y - limbLength * 2 * getSin(theta2);
    } else {
      hand.x = cursorInContainer.x;
      hand.y = cursorInContainer.y;
    }

κ²°κ³Ό

  • μ‚¬μš©μžκ°€ μ»€μ„œλ₯Ό λΉ λ₯΄κ²Œ μ›€μ§μ—¬μ„œ μ»€μ„œ μœ„μΉ˜κ°€ 손을 쑰금 λ²—μ–΄λ‚˜λ„ 손이 μ»€μ„œ μͺ½μœΌλ‘œ μ΄λ™ν•©λ‹ˆλ‹€.
  • νŒ” 길이보닀 멀리 손을 λ“œλž˜κ·Έν•΄λ„ 손은 νŒ”μ— λΆ™μ–΄μžˆλ˜, μ»€μ„œ λ°©ν–₯으둜 μ›€μ§μž…λ‹ˆλ‹€.

νŒ”λ‹€λ¦¬κ°€ ν•œ 번 νŽ΄μ§„ 후에 λ‹€λ₯Έ μͺ½ 손/λ°œμ„ μ›€μ§μ—¬μ„œ νŽ΄μ§„ 뢀뢄을 ꡽힐 수 μžˆμ„κΉŒ?

문제점

  • (λ“œλž˜κ·Έλ₯Ό ν•˜κ±°λ‚˜ 쀑λ ₯에 μ˜ν•΄ νŒ”/닀리가 μ•„λž˜λ°©ν–₯으둜 λ–¨μ–΄μ Έμ„œ) ν•œμͺ½ νŒ”/닀리가 νŽ΄μ§„ ν›„, λ‹€λ₯Έ 손/λ°œμ„ λ“œλž˜κ·Έν–ˆμ„ λ•Œ ν•œ 번 νŽ΄μ§„ νŒ”λ‹€λ¦¬κ°€ κ΅½ν˜€μ§€μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.
  • νŽ΄μ§„ μͺ½μ˜ 손/λ°œμ„ 직접 λ“œλž˜κ·Έν•΄μ„œ κ΅½νžˆλŠ” 방법도 μžˆμ—ˆμœΌλ‚˜, μ΄λŠ” μ‚¬μš©μž μž…μž₯μ—μ„œ λΆˆνŽΈν–ˆμŠ΅λ‹ˆλ‹€.

원인

  • ν•œμͺ½ νŒ”/닀리가 νŽ΄μ§„ ν›„ λ‹€λ₯Έ 손/λ°œμ„ λ“œλž˜κ·Έν–ˆμ„ λ•Œ λͺΈν†΅μ„ μ›€μ§μ΄λŠ” ν•¨μˆ˜κ°€ λ™μž‘ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.
  • λ“œλž˜κ·Έλ₯Ό 멀리 해도 μ‹ μ²΄λΆ€μœ„κ°€ λͺΈμ—μ„œ λ–¨μ–΄μ Έλ‚˜κ°€μ§€ μ•Šλ„λ‘ ν•˜κΈ° μœ„ν•΄, λͺΈν†΅ μ›€μ§μž„ κ΄€λ ¨ ν•¨μˆ˜λŠ” ν•œμͺ½ νŒ”λ‹€λ¦¬κ°€ νŽ΄μ§€λ©΄ λ™μž‘μ„ ν•˜μ§€ μ•Šλ„λ‘ μ„€κ³„λ˜μ–΄μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²° 방법

  • μ‹ μ²΄λΆ€μœ„λ₯Ό μž¬μ •λ ¬μ‹œν‚€λŠ” ν•¨μˆ˜ rearragneBody()λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

  • pointerupμ΄λ²€νŠΈκ°€ λ°œμƒν–ˆμ„ λ•Œλ‚˜ 쀑λ ₯에 μ˜ν•΄ νŒ”/닀리가 μΌμ§μ„ μœΌλ‘œ νŽ΄μ§€κ³  λ‚œ 후에 이 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€.

  • rearragneBody()μ—μ„œ μ–΄κΉ¨/κ³ κ΄€μ ˆ κΈ°μ€€μœΌλ‘œ 손/발이 μœ„μΉ˜ν•œ λ°©ν–₯을 flagλ³€μˆ˜μ— ν• λ‹Ήν•˜κ³ , moveBodyTo()ν•¨μˆ˜λ₯Ό μ‹€ν–‰μ‹œμΌœ μ‹ μ²΄λΆ€μœ„λ“€μ„ μž¬μ •λ ¬ν•©λ‹ˆλ‹€.

    function rearrangeBody(part) {
      if (!attachedStatus.leftHand && dragTarget !== leftHand) {
        leftHand.position.set(leftShoulder.x, leftShoulder.y + armLength * 2 - 2);
      } else if (!attachedStatus.rightHand && dragTarget !== rightHand) {
        rightHand.position.set(
          rightShoulder.x,
          rightShoulder.y + armLength * 2 - 2
        );
      } else if (!attachedStatus.leftFoot && dragTarget !== leftFoot) {
        leftFoot.position.set(leftCoxa.x, leftCoxa.y + legLength * 2 - 2);
      } else if (!attachedStatus.rightFoot && dragTarget !== rightFoot) {
        rightFoot.position.set(rightCoxa.x, rightCoxa.y + legLength * 2 - 2);
      }
    
      if (!part) return;
    
      const flag = { x: null, y: null };
      flag.x = part.hand.x < part.shoulder.x ? -1 : 1;
      flag.y = part.hand.y < part.shoulder.y ? -1 : 1;
    
      exceededPart = null;
      const rearrangePX = 3;
    
      moveBodyTo({
        x: body.x + rearrangePX * flag.x + BODY.WIDTH / 2,
        y: body.y + rearrangePX * flag.y + BODY.HEIGHT / 2,
      });
    }

κ²°κ³Ό

  • νŒ”/닀리가 νŽ΄μ§„ ν›„ pointerupμ΄λ²€νŠΈκ°€ λ°œμƒν•  λ•Œ,
    rearragneBody ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•΄μ„œ νŽ΄μ§„ 뢀뢄을 쑰금 ꡽힘으둜써 λ‹€μŒ λ“œλž˜κ·Έ λ™μž‘μ„ λ‚˜νƒ€λ‚Ό 수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

4. μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό λΆ€λ“œλŸ½κ²Œ λ‚˜νƒ€λ‚΄κΈ°

이 κ²Œμž„μ—μ„œλŠ” 쀑λ ₯에 μ˜ν•œ μ• λ‹ˆλ©”μ΄μ…˜ νš¨κ³Όκ°€ μžˆμŠ΅λ‹ˆλ‹€. (νŒ”/닀리가 μ•„λž˜λ‘œ 떨어짐, 두 손을 λ†“μ•˜μ„ λ•Œ μ•„λž˜λ‘œ 좔락함)
이λ₯Ό λΆ€λ“œλŸ½κ²Œ λ‚˜νƒ€λ‚΄κΈ° μœ„ν•΄μ„œ μ²˜μŒμ—λŠ” setInterval()을 μ‚¬μš©ν–ˆμœΌλ‚˜ λͺ‡κ°€μ§€ 차이점 λ•Œλ¬Έμ— requestAnimationFrame()으둜 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.

setInterval() vs requestAnimationFrame()

μ΄ˆλ‹Ή 호좜 횟수

  • setInterval()은 인자λ₯Ό λ„˜κ²¨ μ΄ˆλ‹Ή 호좜 횟수λ₯Ό 지정할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • rAF()λŠ” λΈŒλΌμš°μ €μ˜ λ¦¬μ†ŒμŠ€ & μ»΄ν“¨ν„°μ˜ CPU μ„±λŠ₯을 κ³ λ €ν•˜μ—¬ μ΄ˆλ‹Ή μ‹€ν–‰νšŸμˆ˜κ°€ κ²°μ •λ©λ‹ˆλ‹€.(κΈ°λ³Έ 60FPS)

μ‹€ν–‰ 방식

  • setInterval()둜 μ• λ‹ˆλ©”μ΄μ…˜μ„ λ§Œλ“€ λ•ŒλŠ”, funcκ³Ό delay만 μ„€μ •ν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€.
    setInterval(func, delay);
  • rAF()의 경우, rAF()의 callbackλ‚΄λΆ€μ—μ„œ rAF()λ₯Ό μž¬ν˜ΈμΆœν•΄μ€˜μ•Ό μ• λ‹ˆλ©”μ΄μ…˜ 싀행이 κ°€λŠ₯ν•©λ‹ˆλ‹€.
    requestAnimationFrame(render);
    
    function render() {
      ...
      requestAnimationFrame(render);
    }

μ‹€ν–‰ 쀑단 방식

  • setInterval()은 κ³ μœ ν•œ id값을 λ¦¬ν„΄ν•˜λ―€λ‘œ, clearInterval()에 ν•΄λ‹Ή id값을 λ„˜κ²¨μ£Όλ©΄ 쀑단 κ°€λŠ₯ν•©λ‹ˆλ‹€.
  • rAF()도 κ³ μœ ν•œ id값을 λ¦¬ν„΄ν•˜λŠ”λ°, 이 id값을 cancelAnimationFrame()에 λ„˜κ²¨μ£Όλ©΄ 쀑단 κ°€λŠ₯ν•©λ‹ˆλ‹€.

ν”„λ ˆμž„μ˜ λΆ€λ“œλŸ¬μ›€

  • setInterval()으둜 κ΅¬ν˜„ν•œ μ• λ‹ˆλ©”μ΄μ…˜μ€ μ•½κ°„μ˜ ν”„λ ˆμž„ λŠκΉ€μ΄ λ°œμƒν•˜κ±°λ‚˜ ν”„λ ˆμž„ 자체λ₯Ό λΉ λœ¨λ¦¬λŠ” λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • rAF()은 μ• λ‹ˆλ©”μ΄μ…˜μ„ μœ„ν•΄ μ΅œμ ν™”λœ ν•¨μˆ˜μ΄λ―€λ‘œ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μ‹€ν–‰λ˜λŠ” ν™˜κ²½μ— 관계 없이 μ μ ˆν•œ ν”„λ ˆμž„ μ†λ„λ‘œ μ‹€ν–‰λ˜λ©°, 탭이 ν™œμ„±ν™”λ˜μ§€ μ•Šμ€ μƒνƒœμ΄κ±°λ‚˜ μ• λ‹ˆλ©”μ΄μ…˜μ΄ νŽ˜μ΄μ§€λ₯Ό λ²—μ–΄λ‚œ κ²½μš°μ—λ„ 계속 μ‹€ν–‰λ˜λŠ” 기쑴의 λ¬Έμ œμ μ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ°±κ·ΈλΌμš΄λ“œ 호좜 μ—¬λΆ€

λΈŒλΌμš°μ €μ—μ„œ μ—¬λŸ¬ 탭을 λ„μ›Œλ†“κ³  μžˆμ„ λ•Œ ν˜„μž¬ μ›ΉνŽ˜μ΄μ§€κ°€ λΉ„ν™œμ„±ν™”λ˜μ–΄μžˆμœΌλ©΄

  • setInterval() 은 λ°±κ·ΈλΌμš΄λ“œμ—μ„œ ν˜ΈμΆœλ˜λŠ” μˆœκ°„λ§ˆλ‹€ 계속 μ‹€ν–‰λ˜μ§€λ§Œ
  • rAF()은 화면에 repaintκ°€ 일어날 λ•Œ ν˜ΈμΆœλ˜λ―€λ‘œ λ°±κ·ΈλΌμš΄λ“œμ—μ„œ ν˜ΈμΆœλ˜μ§€ μ•Šκ³  λŒ€κΈ°ν•©λ‹ˆλ‹€.

requestAnimationFrame()을 μ„ νƒν•œ 이유

  • λ¦¬λ Œλ”λ§μ΄ λλ‚˜μ§€ μ•Šμ•˜λŠ”λ° μ• λ‹ˆλ©”μ΄μ…˜μ„ μˆ˜ν–‰ν•˜λŠ” λͺ…령이 내렀진닀면 μ›ν•˜λŠ”λŒ€λ‘œ μ• λ‹ˆλ©”μ΄μ…˜μ΄ λΆ€λ“œλŸ½κ²Œ μ§„ν–‰λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
    λ¦¬νŽ˜μΈνŠΈκ°€ λλ‚œ ν›„ μ μš©ν•  μ• λ‹ˆλ©”μ΄μ…˜μ„ requestAnimationFrame()의 콜백으둜 λ„£μ–΄μ£Όλ©΄ μžμ—°μŠ€λŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜μ΄ μƒμ„±λ©λ‹ˆλ‹€.
  • setInterval()κ³Ό 달리 ν”„λ ˆμž„ 생성 초기 단계에 맞좰 μ• λ‹ˆλ©”μ΄μ…˜μ΄ ν˜ΈμΆœλ˜μ–΄μ„œ 더 λΆ€λ“œλŸ¬μš΄ λ™μž‘μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.

Tech stack

Frontend

  • React
  • React router
  • Redux-toolkit
  • Styled Components
  • Pixi.js
  • ESLint
  • Jest

Backend

  • Node.js
  • Express
  • MongoDB Atlas / Mongoose
  • ESLint

Pixi.JSλ₯Ό μ„ νƒν•œ 이유

  • μ„±λŠ₯
    • WebGL 2D λ Œλ”λ§μ— κ΄€λ ¨λœ κΈ°λŠ₯λ“€λ§Œ λ“€μ–΄μžˆκΈ° λ•Œλ¬Έμ— ꡉμž₯히 λΉ λ₯΄κ³  κ°€λ³μŠ΅λ‹ˆλ‹€.
  • 크둜슀 ν”Œλž«νΌ ν˜Έν™˜μ„±
    • λ‹€μ–‘ν•œ ν”Œλž«νΌκ³Ό κΈ°κΈ°μ—μ„œ μ›ν™œν•˜κ²Œ μž‘λ™ν•˜λ„λ‘ μ„€κ³„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
  • μ‚¬μš©μ˜ νŽΈμ˜μ„±
    • 직관적이고 κ°„λ‹¨ν•œ APIλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
    • κ³΅μ‹λ¬Έμ„œμ— 정리가 잘 λ˜μ–΄μžˆκ³  μ˜ˆμ œκ°€ ν’λΆ€ν•©λ‹ˆλ‹€.

NoSQL인 MongoDBλ₯Ό μ„ νƒν•œ 이유

  • μŠ€ν‚€λ§ˆ μœ μ—°μ„±
    • SQL에 λΉ„ν•΄ μŠ€ν‚€λ§ˆκ°€ μœ μ—°ν•˜λ©°, λ‹€μ–‘ν•œ 데이터 μœ ν˜•κ³Ό ꡬ쑰λ₯Ό μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • 정해진 ꡬ쑰가 μ—†μœΌλ―€λ‘œ 데이터 ꡬ쑰가 자주 μΆ”κ°€, μ‚­μ œ, λ³€κ²½λ˜λŠ” 경우 μœ μ—°ν•˜κ²Œ μ μš©μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 직관적인 데이터 λͺ¨λΈ
    • 데이터λ₯Ό ν–‰(row) λŒ€μ‹  λ„νλ¨ΌνŠΈ(document)에 μ €μž₯ν•˜κ³ , μ΄λŠ” JSON에 κΈ°λ°˜ν•©λ‹ˆλ‹€. λ”°λΌμ„œ μ—¬λŸ¬ ν…Œμ΄λΈ” κ°„μ˜ λ³΅μž‘ν•œ 쑰인 μ—°μ‚° 없이 λ°μ΄ν„°μ˜ 계측 ꡬ쑰λ₯Ό μ‰½κ²Œ νŒŒμ•…ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • Scale out ꡬ쑰
    • μˆ˜ν‰μ  ν™•μž₯이 κ°€λŠ₯ν•˜λ©°, λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ—¬λŸ¬ λŒ€μ˜ μ„œλ²„μ— λΆ„μ‚°μ‹œν‚΄μœΌλ‘œμ¨ μš©λŸ‰μ„ 늘릴 수 μžˆμŠ΅λ‹ˆλ‹€.

Feature

*ν™€λ“œ: μ†λ°œλ‘œ μž‘μ„ 수 μžˆλŠ” 벽에 λΆ™μ–΄μžˆλŠ” 돌 ν˜•νƒœλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.

  • ν•˜λŠ˜μƒ‰ 배경에 μžˆλŠ” λͺ¨λ“  물체듀은 μž‘κ±°λ‚˜ λ°Ÿμ„ 수 μžˆλŠ” ν™€λ“œμž…λ‹ˆλ‹€.
  • μ‚¬μš©μžλŠ” ν”Œλ ˆμ΄μ–΄μ˜ 손/발/λͺΈν†΅μ„ λ“œλž˜κ·Έν•΄μ„œ 움직일 수 μžˆμŠ΅λ‹ˆλ‹€.
  • ν”Œλ ˆμ΄μ–΄μ˜ 손/λ°œμ„ λ“œλž˜κ·Έν•΄μ„œ ν™€λ“œ μœ„μ— λ†“μœΌλ©΄ κ³ μ •λ©λ‹ˆλ‹€. 그렇지 μ•ŠμœΌλ©΄ 손/발이 μ•„λž˜λ‘œ λ–¨μ–΄μ§‘λ‹ˆλ‹€.
  • λ¬΄κ²Œμ€‘μ‹¬μ˜ xμ’Œν‘œκ°€ μ–‘ 발 사이에 μ—†μœΌλ©΄ ν•œμͺ½ νŒ”μ΄ 펴질 λ•ŒκΉŒμ§€ λ¬΄κ²Œμ€‘μ‹¬μ΄ 쀑λ ₯을 λ°›μ•„μ„œ μ•„λž˜λ‘œ λ‚΄λ €κ°‘λ‹ˆλ‹€.
  • 손/발이 ν™€λ“œμ—μ„œ λ–¨μ–΄μ§€κ±°λ‚˜ λ¬΄κ²Œμ€‘μ‹¬μ΄ μ–‘ 발 사이에 μ—†μœΌλ©΄, 체λ ₯(HP) μ†Œλͺ¨κ°€ 큰 κ²ƒμœΌλ‘œ κ°„μ£Όν•˜μ—¬ HPκ°€ 빨리 μ€„μ–΄λ“­λ‹ˆλ‹€.
  • μ–‘μ†μœΌλ‘œ TOPν™€λ“œλ₯Ό 작으면 μ™„λ“±(성곡)이며, λž­ν‚Ήμ •λ³΄μ— 기둝이 λ“±λ‘λ©λ‹ˆλ‹€.
  • λž­ν‚Ήμ€ λ“±λ°˜ μ‹œκ°„μ΄ 짧은 μˆœμ„œλŒ€λ‘œ 높아지며, 같은 μ‹œκ°„μΌ 경우 HPκ°€ 많이 남은 μ‚¬λžŒμ΄ μˆœμœ„κ°€ λ†’μ•„μ§‘λ‹ˆλ‹€.

Timeline

ν”„λ‘œμ νŠΈ κΈ°κ°„: 2023.04.03(μ›”) ~ 2023.04.28(금)

  • 1 μ£Όμ°¨: 기획 및 섀계
  • 2~3 μ£Όμ°¨: κΈ°λŠ₯ 개발
  • 4 μ£Όμ°¨: ν…ŒμŠ€νŠΈμ½”λ“œ μž‘μ„±, λ°œν‘œ

Video

썸넀일을 ν΄λ¦­ν•˜λ©΄ κ²Œμž„ μ‹œμ—° μ˜μƒ 유튜브 링크둜 μ΄λ™ν•©λ‹ˆλ‹€.

πŸ”½ 첫번째 루트

Video Label

Repository Link

Server


Memoir

Canvas API둜 κ²Œμž„μ„ λ§Œλ“  것은 이번이 μ²˜μŒμ΄μ—ˆμŠ΅λ‹ˆλ‹€. 그리고 κΈ°λŠ₯ κ°œλ°œμ„ ν•  λ•Œ μ°Έκ³ ν•  λ§Œν•œ λΉ„μŠ·ν•œ ν”„λ‘œμ νŠΈκ°€ μ—†μ—ˆκ³ , κ΄€μ ˆ μ›€μ§μž„ 및 물리엔진 κ΄€λ ¨ 라이브러리 없이 μˆœμˆ˜ν•˜κ²Œ 제 λ…Όλ¦¬λ‘œ κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λŠ” 것은 쉽지 μ•Šμ•˜μŠ΅λ‹ˆλ‹€. κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³ , ν•¨μˆ˜μ˜ μž¬μ‚¬μš©μ„±μ„ κ³ λ €ν•˜λ©° μ°¨κ·Όμ°¨κ·Ό λ‘œμ§μ„ κ΅¬μ„±ν•˜κ³  μƒμ†Œν•œ 문제λ₯Ό ν•΄κ²°ν•˜λŠ” 과정은 λΏŒλ“―ν•œ κ²½ν—˜μ΄μ—ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” 클라이밍을 정말 μ’‹μ•„ν•©λ‹ˆλ‹€. 특히 μ™„λ“± λͺ» ν•  것 같은 루트λ₯Ό μ„±κ³΅ν–ˆμ„ λ•Œ 성취감은 제 μ‚Άμ˜ 원동λ ₯의 일뢀가 λ˜κΈ°λ„ ν•©λ‹ˆλ‹€.
이 κ²Œμž„μ„ 톡해 λ‹€λ₯Έ μ‚¬λžŒλ“€λ„ λΆˆκ°€λŠ₯ν•΄λ³΄μ΄λŠ” λͺ©ν‘œλ₯Ό μ„±μ·¨ν•˜λŠ” 즐거움을 느끼면 μ’‹κ² μŠ΅λ‹ˆλ‹€.

About

πŸ§— A game where you can click on the player's hands and feet to climb

https://climick.netlify.app/


Languages

Language:JavaScript 97.4%Language:CSS 1.7%Language:HTML 0.8%Language:Shell 0.1%