728x90

GLB 파일 압축하기

웹에서 클라이언트가 웹서버에 리소스 요청을 할 때,  glb는 굉장히 많은 비용을 발생시킵니다. 이는 분명 성능 저하에도 영향을 준다고 생각이 들어서, 알아보았고 Draco를 통해서 glb파일을 압축할 수 있다는 사실을 알게 되었습니다.

 

Draco는 구글에서 개발한 3d파일 압축 기술이면  어떻게 압축을 효율적으로 하는지는 아래 논문에서 자세히 볼 수 있습니다..

(개인적으로 너무 어려워서 머리에 잘 담기진 않네요 ㅠㅠ)

https://www.kibme.org/resources/journal/20230807171156346.pdf

 

그리고 이를 쉽게 사용할 수 있게 하는 오픈 소스라이브러인 'glTF Pipeline' 을 사용해서 압축하였습니다.

 

이중 내가 사용할 명령어는 -i (변경할 파일)  -o (변경된 파일명)  -d  (드라코 압축 옵션) 입니다.

gltf-pipeline -i model.gltf -o modelDraco.gltf -d
 
이를 바탕으로 압축하였을 때, 아래와 같이 background 같이 큰 파일은 50% 가까이 압축되었고, 그 외에도 10~15프로 정도 압축이 되었습니다.

(기존 파일 명을 2라고 수정하고 변경완료된 파일명을 기존 파일명으로 했습니다.)

 

압축 전 후 비교

 

이제 설레는 마음을 가지고 얼만큼 절약이 되었을지 확인만 하면 됩니다.

 

퍼포먼스 탭을 통해서 성능 비교하기

우선 next.js에 layout에 있는 header가 먼저 렌더링 됩니다.

이후 page.js를 요청하고, 현재 suspense로 캔버스의 loading 처리를 하고 있습니다. 그리고 Shiba에서 shiba glb파일, Background에서 background.glb 파일을 사용하고 있습니다.

export default function LuckyShiba() {
  return (
    <div className="wrapper_3d">
      <Suspense fallback={<Loading />}>
        <SessionProvider>
          <CanvasLayout color={color.bg} camera={{ position: [0, 2, 4] }}>
            <Physics gravity={[0, -9.8, 0]}>
              <ambientLight />
              <directionalLight position={[0, 5, 5]} />
              <Shiba />
              <Walls />
              <Background />
              <TutorialOpener />
            </Physics>
          </CanvasLayout>
        </SessionProvider>
      </Suspense>
    </div>
  );
}

 

비교를 명확하기 위해서 실험은 빠른 3G 환경에서 하였습니다.

 

 

현재 제 사이트에서 새로 고침 후 html을 서버에 요청을 하게 되면 header와 관련된 부분과 page.js를 요청하고 header가 가볍기에 먼저 렌더링이 됩니다. 이후 page.js 요청이 끝나면 loading 처리가 시작되면서 glb파일들을 요청을 합니다.

 

사진이 좀 작아서  값을 표기하면 크기 시간 순으로 background.glb를 압축 전 후 비교하면

압축 후 : 246B , 624 밀리초 

압축 전 : 246B , 651 밀리초

 

생각보다 차이가 나지 않아서 여러번 했었느데, 위 시간과 큰 차이가 나진 않더군요..

분명 git에서 Bin파일이 압축되었음을 확인에도 불구하고 제가 생각한 것 이상의 차이는 않네요..

 

 

좀 더 자세한 비교를 위해서 성능 탭을 보았는데도 유의미한 결과를 호가인하진 못했습니다.

 

 

 

결론

1. 또한 glb파일보다 canvas 가 들어 있는 page.js 파일을 받아오는데 더 많은 리소스를 차지 (약 55배?) 확인할 수 있었습니다.

2. 성능의 차이가 존재하지만, 실제 압축된 용량에 비해서는 성능의 차이가 생각보다 크지 않았다.
--> 왜에 대해서 분석하고 싶은데,, 당장은 결론을 얻진 못헀습니다..

 

 

 

 

728x90

최근 프로젝트의 기능 구현이 마무리가 되었습니다. 하지만 아직 클린코드의 관점에서 미숙한 부분이 많습니다.

이를 값, 계산 , 액션으로 분리하기와  solid원칙을 준수하기를 바탕으로 리팩토링하려고 합니다.

 

코드 원문과 이미지는 아래와 같습니다.

import { useState } from 'react';
import Tutorial1 from './Tutorial1';
import Tutorial2 from './Tutorial2';
import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { MANUAL_SKIP } from '@/shared/contants';

export default function GuidePopUp() {
  const [step, setStep] = useState<number>(0);
  const stepChanger = (direction: 'before' | 'after') => {
    const lastStep = 1;
    if (direction === 'before') {
      if (step > 0) {
        setStep(step - 1);
      }
    } else {
      if (step < lastStep) {
        setStep(step + 1);
      }
    }
  };

  const handleDontShowAgain = () => {
    localStorage.setItem(MANUAL_SKIP, 'true');

    const escEvent = new KeyboardEvent('keydown', { key: 'Escape' });
    document.dispatchEvent(escEvent);
  };

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>
        {step === 0 && <Tutorial1 />}
        {step === 1 && <Tutorial2 />}
      </div>
      <button className={styles.hideButton} onClick={handleDontShowAgain}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

이런 지저분한 코드를 관심사를 분리해서

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { tutorials } from './content';
import { useStep } from './hook/useStep';
import HideButton from './control/HideButton';
import Navigation from './control/Navigation';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>
      <HideButton />
      <Navigation step={step} stepChanger={stepChanger} totalSteps={lastStep} />
    </div>
  );
}

이렇게 만드는 과정입니다.

 

그러기 위해선 우선 화면을 관심사에 따라서 세분화 합니다.

 공통적으로 존재하는 제목 영역, step에 따라 바뀌어야 하는 내용영역, 그리고 공통적으로 존재하는 버튼과 현재 step 영역이 존재합니다.

 

내용영역 분리하기

이 중에서 우선 가운데의 내용부분을 분리하려고 합니다.

 

내용의 경우에는 줄바꿈이나, 강조하고 싶은 색깔이 다르기 때문에 공통적인 요소로 묶기 힘드니, 개별적인 컴퍼넌트로 작성하였습니다. 이때 내용의 경우에는 html과 css를 활용해서 꾸민 '값'입니다.

따라서 Popup 컴퍼넌트에서 값을 분리하였습니다.

 

import Tutorial1 from './Tutorial1';
import Tutorial2 from './Tutorial2';

export const tutorials = [<Tutorial1 key={1} />, <Tutorial2 key={2} />];

 

import { useState } from 'react';

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { hideManualByDefault } from './util';
import { tutorials } from './content';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, setStep] = useState<number>(0);

  const stepChanger = (direction: 'before' | 'after') => {
    if (direction === 'before') {
      if (step > 0) {
        setStep(step - 1);
      }
    } else {
      if (step < lastStep) {
        setStep(step + 1);
      }
    }
  };

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>

      <button className={styles.hideButton} onClick={hideManualByDefault}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

 

이제 Tutorial 페이지가 늘어나도,  GuidePopup의 코드에는 아무런 영향이 가지 않습니다.

또한 기존에는 직접 lastStep을 지정해줬는데, 배열의 길이를 통해 마지막 단계가 어딘지 구하는 로직으로 변경되었습니다.

카카오의 아티클을 보고 다형성 기반으로 개방 폐쇠 법칙도 적용하고 싶었는데, 외부에서 불러오는 데이터도 아니고 단순한 값이기에 그 정도의 깊이 있는 분리가 나오지는 않았네요..

 

커스텀 훅으로 튜토리얼 단계변경 액션 분리하기

import { useState } from 'react';

export const useStep = (lastStep: number) => {
  const [step, setStep] = useState<number>(0);
  const stepChanger = (direction: 'before' | 'after') => {
    setStep((prevStep) => {
      if (direction === 'before' && prevStep > 0) {
        return prevStep - 1;
      }
      if (direction === 'after' && prevStep < lastStep) {
        return prevStep + 1;
      }
      return prevStep;
    });
  };

  return [step, stepChanger] as const;
};

 

해당 함수의 관심사는 이전과 다음 단계로 이동시키는 것입니다.

따라서 이를 useStep이라는 훅으로 분리하고, 튜플형태로 return 시켰습니다.

https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state

 

useState – React

The library for web and native user interfaces

react.dev

에서 set 함수의 경우 콜백함수 형태로 사용시 이전 상태를 사용할 수 있습니다. 따라서 이전 상태를 prevStep으로 명명하고 조건문을 얼리 리턴 기법을 사용하여 가독성을 향상시켰습니다.

 

import { useState } from 'react';

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { hideManualByDefault } from './util';
import { tutorials } from './content';
import { useStep } from './hook/useStep';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>

      <button className={styles.hideButton} onClick={hideManualByDefault}>
        다시 보지 않기
      </button>
      <div className={styles.naviWrapper}>
        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('before');
          }}
        >
          이전
        </button>
        {step + 1 + '/' + 2}

        <button
          className={styles.naviButton}
          onClick={() => {
            stepChanger('after');
          }}
        >
          다음
        </button>
      </div>
    </div>
  );
}

 

이제 마지막으로  버튼 영역을 분리하려고 합니다.

 버튼 영역 분리하기

우선 숨기기 버튼부터 분리하였습니다.

import React from 'react';
import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { MANUAL_SKIP } from '@/shared/contants';
import { closeModalByEvent } from '@/shared/components/portal/util';

export default function HideButton() {
  const hideManualByDefault = () => {
    localStorage.setItem(MANUAL_SKIP, 'true');
    closeModalByEvent();
  };

  return (
    <button className={styles.hideButton} onClick={hideManualByDefault}>
      다시 보지 않기
    </button>
  );
}

기존에는 util 폴더에서 hideManualByDefault 함수를 관리하였는데, 숨기기 버튼이 분리되면서 굳이 따라 관리할 필요성을 느끼지 못했습니다. 따라서 해당 함수를 HideButton 컴퍼넌트와 결합시켰습니다.

 

이후 버튼도 page 변경용도의 버튼들이기에 Navigation이라는 이름으로 분리시켰습니다.

import React from 'react';
import styles from '@/app/luckyshiba/luckyshiba.module.css';

interface NavigationProps {
  step: number;
  stepChanger: (direction: 'before' | 'after') => void;
  totalSteps: number;
}

export default function Navigation({
  step,
  stepChanger,
  totalSteps,
}: NavigationProps) {
  return (
    <div className={styles.naviWrapper}>
      <button
        className={styles.naviButton}
        onClick={() => stepChanger('before')}
      >
        이전
      </button>
      {step + 1 + '/' + totalSteps}
      <button
        className={styles.naviButton}
        onClick={() => stepChanger('after')}
      >
        다음
      </button>
    </div>
  );
}

 

최종적으로 완성된 컴퍼넌트는

import styles from '@/app/luckyshiba/luckyshiba.module.css';
import { tutorials } from './content';
import { useStep } from './hook/useStep';
import HideButton from './control/HideButton';
import Navigation from './control/Navigation';

export default function GuidePopUp() {
  const lastStep = tutorials.length;
  const [step, stepChanger] = useStep(lastStep);

  return (
    <div className={styles.manualWrapper}>
      <div className={styles.manualTitle}>게임 시작 메뉴얼</div>
      <div className={styles.manualContents}>{tutorials[step]}</div>
      <HideButton />
      <Navigation step={step} stepChanger={stepChanger} totalSteps={lastStep} />
    </div>
  );
}

 

느낀점

원티드 프리온보딩에서 멘토님께 배운 값, 계산, 액션으로 분리하고 최근 아티클을 읽은 내용을 바탕으로 solid하게 작성하고자 하였습니다.

 

간단한 메뉴얼 컴퍼넌트였기에 SOLID패턴중에서 아티클에서 읽고 느낀 모든 내용을 반영하지는 못하였습니다.

하지만 해당 아티클에서 말하였듯이 SOLID에서 가장 핵심은 Single Responsibility Principle 이며, 관심사의 분리에 집중하여 리팩토링을 진행하였습니다.

또한 SOLID원칙을 요약하는건 간단하지면 각각의 키워드에 맞게 작성하는 과정은 좀 더 깊이 공부하고 생각할 필요성을 느꼈습니다.

아직 완벽하진 않을 수 있지만, 초기 코드에 비해서는 훨씬 가독성이 향상되었다는 점에서 보람을 느꼈습니다.

 

참고문헌

https://fe-developers.kakaoent.com/2023/230330-frontend-solid/

 

프론트엔드와 SOLID 원칙 | 카카오엔터테인먼트 FE 기술블로그

임성묵(steve) 판타지, 무협을 좋아하는 개발자입니다. 덕업일치를 위해 카카오페이지로의 이직을 결심했는데 인사팀의 실수로 백엔드에서 FE개발자로 전향하게 되었습니다. 인생소설로는 데로

fe-developers.kakaoent.com

https://www.perssondennis.com/articles/write-solid-react-hooks

 

Persson Dennis - Write SOLID React Hooks

Write SOLID React Hooks June 2, 2024 (19 minutes reading) SOLID is one of the more commonly used design patterns. It's commonly used in many languages and frameworks, and there are some articles out there how to use it in React as well. Each React article

www.perssondennis.com

 

원티드 프리온보 오종택 멘토님 강의

 

728x90

들어가며

초기에는 간단하게 position을 통해서 전진 후진 회전만 구현하였습니다.

https://ungumungum.tistory.com/121

 

R3F에서 시바를 키보드 입력을 통해 회전시키기(트러블 슈팅)

들어가며좀 더 interactive한 웹을 만들기 위해서 최근 three.js 기반의 R3F 라이브러리를 활용하여, 웹 페이지를 만들고 있습니다.강아지를 활용하여서 이동하게 하려고 하는데, 강아지의 이동 및 회

ungumungum.tistory.com

 

총 4가지의 방법을 도입했었고, 결국 어떤 방법을 도입했는지에 대한 설명을 하고자 합니다.

1. position 기반 이동시키기

2. velocity 기반 이동시키기

3. force 기반 이동시키기

4. chassicBody를 이용한 자동차형으로 이동시키기

 

그리고 R3F에서 위치를 이동시키기 위해선 키보드 이벤트를 감지하여 이동시켜야 합니다. 

이때 useInput 커스텀훅을 사용하여서 wasd space를 감지하였고,, 이를 기반으로 이동을 시키는데, 2가지 방식으로 이동이 가능합니다.

1. useEffect : 상태가 변화했을 때 실행
    2번과 4번같은 경우에는 물체의 속도를 정하고, 바퀴의 회전속도를 정하여서 이동합니다. 따라서 전진 후진등의 버튼의      상태가 변화했을 때 적용시키면 됩니다.

2. useFrame : 매 프레임마다 상태를 확인하여 실행

    1번과 3번의 position을 이동시키거나 힘을 가하는 방식에서는 매 프레임 확인하여 위치를 변경해야합니다.

 

이 중에서 어떤 방식을 왜 기각했고, 결국 왜 position 기반 이동 방식을 도입하게 되었는지에 대해서 작성하고자 합니다.

 

제가 고민 및 도입했던 과정은 1 4 3 2 1 순이며, 1번의 경우 앞선 글에서 설명했으니, 4 3 2 1 순으로 설명하고자 합니다.

 

각각의 방식 도입과 기각 이유

chassicBody를 이용한 자동차형으로 이동

도입배경은 useCannon에서 충돌체가 불안정하기 때문입니다. 참고 : https://ungumungum.tistory.com/122

 

R3F에서 물체가 지나갈 수 있는 언덕 만들기(react-three/cannon)

아래와 같은 환경에서  언덕을 만들어주려고 합니다.cannon-es 공식문서에서 간단하게 Trimesh를 이용해서 삼각형을 만드는 법을 알려주는데,  이를 기반으로 react-three/cannon에서 삼각형 모양의 기

ungumungum.tistory.com

 

box나 cylinder 이외의 다른 충돌체의 경우 충돌이 불안정하였고, 이중 cylinder가 타 충돌체와의 상호작용이 안정적이었습니다.

따라서 다리를 만들어하는 제 입장에서 바퀴가 cylinder인 방식을 도입한다면 좋을 것이라 판단하였습니다.

 

useCannon에서는 기본적으로 자동차 엔진과 유사한 형식으로 작동하는 훅(useRayCastVehicle)을 제공하고 있습니다.

(useCannon은 결국 cannon을 확장해서 만든거라 설명은 cannon-es가 더 자세합니다.)

 

이를 기반으로 바퀴를 조정하는 바퀴 및 이동 물체의 정보를 받아서, 조종하는 훅을 만들었습니다. (관심사 분리가 명확하지 않은점은 죄송합니다 ㅠ.)

//useMoveCar.ts
import { useEffect, useRef } from 'react';
import { useInput } from './useInput';
import {
  PublicApi,
  RaycastVehiclePublicApi,
  useRaycastVehicle,
} from '@react-three/cannon';
import { Group, Object3DEventMap, Quaternion, Vector3 } from 'three';
import { useWheels } from './useWheels';

type MoveCarProps = {
  chassisApi: PublicApi;
  chassisBody: React.RefObject<Group<Object3DEventMap>>;
  worldDirection: Vector3;
  bodyInfo: {
    width: number;
    height: number;
    front: number;
  };
};

export const useMoveCar = ({
  chassisApi,
  worldDirection,
  chassisBody,
  bodyInfo,
}: MoveCarProps) => {
  const { forward, backward, left, right, jump, stand } = useInput();
  const engineForce = 400;
  const velocity = useRef([0, 0, 0]);
  const { width, height, front } = bodyInfo;
  const [wheels, wheelInfos] = useWheels({ width, height, front });

  const [vehicle, vehicleApi] = useRaycastVehicle(
    () => ({
      chassisBody,
      wheelInfos,
      wheels,
    }),
    useRef<Group>(null)
  );

  useEffect(() => {
    chassisApi.velocity.subscribe((v) => (velocity.current = v));

    if (forward) {
      vehicleApi.applyEngineForce(engineForce, 2);
      vehicleApi.applyEngineForce(engineForce, 3);
    } else if (backward) {
      vehicleApi.applyEngineForce(-engineForce, 2);
      vehicleApi.applyEngineForce(-engineForce, 3);
    } else {
      vehicleApi.applyEngineForce(0, 2);
      vehicleApi.applyEngineForce(0, 3);
    }

    if (left) {
      vehicleApi.setSteeringValue(0.35, 2);
      vehicleApi.setSteeringValue(0.35, 3);
      vehicleApi.setSteeringValue(-0.1, 0);
      vehicleApi.setSteeringValue(-0.1, 1);
    } else if (right) {
      vehicleApi.setSteeringValue(-0.35, 2);
      vehicleApi.setSteeringValue(-0.35, 3);
      vehicleApi.setSteeringValue(0.1, 0);
      vehicleApi.setSteeringValue(0.1, 1);
    } else {
      for (let i = 0; i < 4; i++) {
        vehicleApi.setSteeringValue(0, i);
      }
    }
    if (jump) {
      const [x, y, z] = velocity.current;
      chassisApi.velocity.set(x * 1.2, y + 7, z * 1.2);
    }

    if (stand) {
      const rotateQuaternion = new Quaternion().setFromAxisAngle(
        new Vector3(0, 1, 0),
        Math.PI / 2
      );
      const currentQuaternion = new Quaternion().setFromUnitVectors(
        new Vector3(1, 0, 0),
        worldDirection
      );
      currentQuaternion.multiply(rotateQuaternion); // y축 회전 적용
      chassisApi.quaternion.set(
        currentQuaternion.x,
        currentQuaternion.y,
        currentQuaternion.z,
        currentQuaternion.w
      ); // 쿼터니언을 직접 설정
    }
  }, [
    backward,
    stand,
    worldDirection,
    chassisApi,
    forward,
    jump,
    left,
    right,
    vehicleApi,
  ]);

  return { vehicle, vehicleApi };
};

 

기능은 총 4가지로 만들었습니다.  
1. 전후진 , 2. 좌우 바퀴회전 , 3.  점프(바퀴가 공중에 뜨기 때문에 x.z축으로 약간의 가속), 4. 서기(전복 시 사용)

 이동과 trimesh로 만든 다리를 건너는 기능까지는 가능합니다. 

 

문제점

하지만 여러 단점이 발견되었고, 결국 일반적인  Box로 움직이도록 하기로 결정하였습니다. 

1. 하지만 문제점이 언덕에서 잘 미끄러짐.

2. 바퀴의 크기가 작으면 몸체가 충돌하여 언덕 잘 못 올라감

3. 바퀴가 크면 언덕을 올라가기전에 종종 충돌해서 멈춤

4. 시바가 자동차처럼 움직여서 부자연스러움

 

force 를 이용한 이동

  const [chassisBody, chassisApi] = useCompoundBody(
    () => ({
      position,
      mass: mass,
      rotation: [0, 0, 0],
      collisionFilterGroup: 5,
      angularDamping: 0.95,
      onCollide: () => {
        !isLanded && setIsLanded(true);
      },
      shapes: [
        {
          args: chassisBodyArgs,
          position: [0, 0, 0],
          type: 'Box',
        },
      ],
    }),
    useRef<Group>(null)
  );
chassicApi.applyForce([x,y,z])

 

다음과 같이 cannon의 충돌체에는 힘을 가할 수 있는 옵션이 있습니다. 이를 바탕으로 시바가 바라보고 있는 방향으로 힘을 가하여 이동시키는 로직에, 회전의 경우 기존 y축 기준으로 회전시키는 방법을 도입하면 어떤가 했습니다.

 

문제점

기본적으로 -y축 방향으로 9.8의 중력이 가해지고 있습니다. 이때 힘을 가하여 이동시키니 시바가 이동하는게 아닌 구르기 시작했습니다.. 

 

그렇기에 회전시에 마찰력을 존재하게 해서 구르지 못하도록 하는 방식을 도입하려고 했습니다.

위의 코드의 useCompoundBody 훅을 보시면 angularDamping이라는 각회전시 마찰력이 존재합니다. 이를 1로 하면 회전하지 않게 되는데, 이런 경우에는 언덕을 못 올라가는 문제가 발생하였습니다..

언덕을 볼 때는 30도 위를 쳐다봐야하는데, 그러지 않으니 힘을 가해도 언덕에 부딪히기만 하는 문제가 생겼습니다.

 

velocity 를 이용한 이동

chassicApi.velocity.set(x,y,z)

마찬가지로 속력을 가하여 이동시키는 방식을 도입하였는데, 해당 방식의 경우에 이동은 제대로 되었습니다.

언덕도 올라갈 수 있고, 적당한 angularDamping (0.95)를 설정시 구르지도 않게 되었습니다.

 

문제점 

하지만  전진할 때 속력을 부여하는 방식은 점프와 같이 쓰기엔 불안정했습니다.

일반적인 이동에서는 y방향으로의 속력을 0으로 설정합니다. 문제는 공중에 떠 있을 때 입니다.

중력의 영향을 받기에 내려가야하는데 , y축의 속력이 0이 아니기에 낙하하는 속력을 구해서 직접 반영해야합니다.

 

매번 shiba의 y축 속력을 구해서 이동시키는건 너무 비효율적인 방식이어서 최종적으로 position을 기반으로 이동하는 방식을 도입하였습니다.

 

 

position 방식과 코드

추후에 이벤트 발생과도 연계해야했기에, 점프를 하고 있다면 전역변수를 통해 착륙하지 않음(isLanded = false)를 만들었고, 타 물체와 충돌 시에 착륙으로 만드는 로직을 구현하였습니다.

import { Group, Object3DEventMap, Quaternion, Vector3 } from 'three';
import { useInput } from './useInput';
import { PublicApi } from '@react-three/cannon';
import { useMemo } from 'react';
import { useShibaStore } from '@/store/shiba';
type MovePositionProps = {
  worldPosition: Vector3;
  worldDirection: Vector3;
  chassisApi: PublicApi;
  chassisBody: React.RefObject<Group<Object3DEventMap>>;
};

export const useMovePosition = ({
  worldDirection,
  worldPosition,
  chassisApi,
  chassisBody,
}: MovePositionProps) => {
  const { forward, backward, left, right, jump, stand } = useInput();
  const worldQuaternion = useMemo(() => new Quaternion(), []);
  const { isLanded, setIsLanded, blockEvent, eventable } = useShibaStore();
  const controlMovement = (delta: number) => {
    if (forward || backward) {
      const speed = delta * 7;
      let { x, y, z } = worldPosition;
      let { x: rx, y: ry, z: rz } = worldDirection;
      let [newX, newY, newZ] = [x, y, z];

      if (forward) {
        newX += rx * speed;
        newZ += rz * speed;
      }
      if (backward) {
        newX -= rx * speed;
        newZ -= rz * speed;
      }
      chassisApi.position.set(newX, newY, newZ);
    }

    if (right || left) {
      const turnAngle = delta;
      const turnQuaternion = new Quaternion();
      chassisBody?.current!.getWorldPosition(worldPosition);

      if (right) {
        turnQuaternion.setFromAxisAngle(new Vector3(0, 1, 0), -turnAngle);
      }
      if (left) {
        turnQuaternion.setFromAxisAngle(new Vector3(0, 1, 0), turnAngle);
      }
      worldQuaternion.multiplyQuaternions(turnQuaternion, worldQuaternion);
      chassisApi.quaternion.set(
        worldQuaternion.x,
        worldQuaternion.y,
        worldQuaternion.z,
        worldQuaternion.w
      );
    }

    if (jump) {
      if (isLanded) {
        setIsLanded(false);
      }
      chassisApi.velocity.set(0, 5, 0);
    }
  };

  return controlMovement;
};

 

+ Recent posts