Try to create top down game from react-three-fiber v4 to v8

This is great tutorial but very old Making a 2D RPG game with react-three-fiber - DEV Community

I dont know how to deal with useUpdate useUpdate | React Three Fiber

where i can find migrate v4 to latest version of r3f

in this file https://github.com/coldi/r3f-game-demo/blob/01243396100f64a822e8af4c102fe6b4c6d36d8c/src/%40core/Graphic.tsx

import { useUpdate } from 'react-three-fiber';

useUpdate is deprecated on latest r3f

I try to convert

    const textureRef = useUpdate<THREE.Texture>((texture) => {
      texture.needsUpdate = true;
    }, []);

to

    const textureRef = useRef<THREE.Object3D>(null!);
    useImperativeHandle(
      ref,
      () => {
        textureRef.current.needsUpdate = true;
        return textureRef.current;
      },
      []
    );

but i think this not right!

because i cant access textureRef.current.needsUpdate = true;

and more!

in this file https://github.com/coldi/r3f-game-demo/blob/01243396100f64a822e8af4c102fe6b4c6d36d8c/src/%40core/Scene.tsx

createPortal(newElement, portalNode, null, key)

createPortal is expected 2-3 arguments, but got 4.

in this file

<Canvas
                camera={{
                    position: [0, 0, 32],
                    zoom: cameraZoom,
                    near: 0.1,
                    far: 64,
                }}
                orthographic
                noEvents
                gl2
                // @ts-ignore
                gl={{ antialias: false }}
                onContextMenu={e => e.preventDefault()}
            >
                <GameContext.Provider value={contextValue}>
                    {children}
                </GameContext.Provider>
            </Canvas>

noEvents and gl2 has been deprecated

see Releases · pmndrs/react-three-fiber · GitHub there were breaking changes but quite minimal and easily migrated to. in your case imo that’s only useUpdate which was just a useLayoutEffect anyway.

as for portaling, i don’t understand what the code is doing there, it seems quite unconventional. a portal renders jsx into a foreign object, like a THREE.Scene, it is commonly used for webgl render targets.

function Foo({ children }) {
  const buffer = useFBO()
  const [scene] = useState(() => new THREE.Scene()
  ...
  useFrame((state) => {
    // render -scene- into off-buffer
    state.gl.setRenderTarget(buffer)
    state.gl.render(scene, state.camera)
    ...
    state.gl.setRenderTarget(null)
  })
  ...
  // Puts children into -scene- 
  return (
    <>
      {createPortal(children, scene)}
      <mesh>
        <planeGeometry />
        {/* Put buffer texture into a material */}
        <meshBasicMaterial map={buffer.texture} />
      </mesh>
    </>
  )
}
<Foo>
  <mesh>
    <boxGeometry />
    <meshBasicMaterial />
  </mesh>
</Foo>

it’s being used for stuff like this

1 Like

Everything is working fine except

in @core/Graphic.tsx

import React, {
  forwardRef,
  memo,
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import * as THREE from "three";
import { Position } from "./GameObject";
import useGameLoop from "./useGameLoop";
import { useLoader } from "@react-three/fiber";
import HtmlOverlay from "./HtmlOverlay";

export interface GraphicProps {
  src: string;
  sheet?: {
    [index: string]: number[][];
  };
  state?: string;
  frameWidth?: number;
  frameHeight?: number;
  frameTime?: number;
  scale?: number;
  flipX?: number;
  color?: string;
  opacity?: number;
  offset?: Position;
  basic?: boolean;
  blending?: THREE.Blending;
  magFilter?: THREE.TextureFilter;
  onIteration?: () => void;
}

export default memo(
  forwardRef<THREE.Mesh, GraphicProps>(function Graphic(
    {
      src,
      sheet = {
        default: [[0, 0]],
      },
      state = "default",
      frameWidth = 16,
      frameHeight = 16,
      frameTime = 200,
      scale = 1,
      flipX = 1,
      color = "#fff",
      opacity = 1,
      offset = { x: 0, y: 0 },
      basic,
      blending = THREE.NormalBlending,
      magFilter = THREE.NearestFilter,
      onIteration,
    }: GraphicProps,
    outerRef
  ) {
    if (!sheet[state]) {
      // eslint-disable-next-line no-console
      console.warn(
        `Sprite state '${state}' does not exist in sheet '${src}':`,
        Object.keys(sheet)
      );
    }

    const [frameState, setFrameState] = useState<any>();

    const texture = useLoader(THREE.TextureLoader, src);
    const textureRef = useRef<THREE.Texture>(null!);

    const mounted = useRef(true);
    const interval = useRef<number>();
    const prevFrame = useRef<number>(-1);
    const frame = useRef(0);
    const frames = sheet[state];
    const [firstFrame, lastFrame = firstFrame] = frames;
    const frameLength = lastFrame[0] + 1 - firstFrame[0];

    const handleFrameUpdate = useCallback(() => {
      const currentFrame = firstFrame[0] + frame.current;
      const textureOffsetX =
        (currentFrame * frameWidth) / textureRef.current.image.width;
      const textureOffsetY =
        (firstFrame[1] * frameHeight) / textureRef.current.image.height;
      textureRef.current.offset.setX(textureOffsetX);
      textureRef.current.offset.setY(textureOffsetY);
      setFrameState({
        state,
        textureOffsetX,
        textureOffsetY,
      });
      console.log("setFrameState", state, currentFrame);
    }, [firstFrame, frameHeight, frameWidth, textureRef.current, frame, state]);

    // initial frame update
    useEffect(() => handleFrameUpdate(), [handleFrameUpdate]);

    useGameLoop((time) => {
      if (!mounted.current) return;
      if (interval.current == null) interval.current = time;

      if (time >= interval.current + frameTime) {
        interval.current = time;
        prevFrame.current = frame.current;
        frame.current = (frame.current + 1) % frameLength;

        handleFrameUpdate();

        if (prevFrame.current > 0 && frame.current === 0) {
          onIteration?.();
        }
      }
    }, frameLength > 1);

    const iterationCallback = useRef<typeof onIteration>();
    iterationCallback.current = onIteration;
    // call onIteration on cleanup
    useEffect(
      () => () => {
        mounted.current = false;
        iterationCallback.current?.();
      },
      []
    );

    const materialProps = useMemo<
      Partial<THREE.MeshBasicMaterial & THREE.MeshLambertMaterial>
    >(
      () => ({
        color: new THREE.Color(color),
        opacity,
        blending,
        transparent: true,
        depthTest: false,
        depthWrite: false,
        fog: false,
        flatShading: true,
        precision: "lowp",
      }),
      [opacity, blending, color]
    );

    const textureProps = useMemo(() => {
      const size = {
        x: texture.image.width / frameWidth,
        y: texture.image.height / frameHeight,
      };
      return {
        repeat: new THREE.Vector2(1 / size.x, 1 / size.y),
        magFilter,
        minFilter: THREE.LinearMipMapLinearFilter,
      };
    }, []);

    useLayoutEffect(() => {
      textureRef.current = texture;
      textureRef.current.repeat = textureProps.repeat;
      textureRef.current.minFilter = textureProps.minFilter;
      textureRef.current.magFilter = magFilter as any;
      const currentFrame = firstFrame[0] + frame.current;
      const textureOffsetX =
        (currentFrame * frameWidth) / textureRef.current.image.width;
      const textureOffsetY =
        (firstFrame[1] * frameHeight) / textureRef.current.image.height;
      textureRef.current.offset.setX(textureOffsetX);
      textureRef.current.offset.setY(textureOffsetY);
    }, [textureProps]);

    //console.log("render");

    // return (
    //   <group>
    //     <HtmlOverlay>
    //       <div style={{ fontSize: "6px" }}>
    //         {frameState ? frameState.state : ""}
    //       </div>
    //       <div style={{ fontSize: "6px" }}>
    //         {frameState ? frameState.textureOffsetX : ""}
    //       </div>
    //       <div style={{ fontSize: "6px" }}>
    //         {frameState ? frameState.textureOffsetY : ""}
    //       </div>
    //     </HtmlOverlay>
    //   </group>
    // );

    return (
      <mesh
        ref={outerRef}
        position={[offset.x, offset.y, -offset.y / 100]}
        scale={[flipX * scale, scale, 1]}
      >
        <planeGeometry attach="geometry" />
        {basic ? (
          <meshBasicMaterial
            attach="material"
            map={texture}
            {...materialProps}
          />
        ) : (
          <meshLambertMaterial
            attach="material"
            map={texture}
            {...materialProps}
          />
        )}
        <group>
          <HtmlOverlay>
            <div style={{ fontSize: "6px" }}>
              {frameState ? frameState.state : ""}
            </div>
            <div style={{ fontSize: "6px" }}>
              {frameState ? frameState.textureOffsetX : ""}
            </div>
            <div style={{ fontSize: "6px" }}>
              {frameState ? frameState.textureOffsetY : ""}
            </div>
          </HtmlOverlay>
        </group>
      </mesh>
    );
  })
);

textureRef.current.offset.setX(textureOffsetX);
textureRef.current.offset.setY(textureOffsetY);

textureRef is problem when i update offset.setX it will update all others texture with same image url too (image url is sprite)

live code here

Code Sandbox

when i interact with work station

it will update other textureRef too

anything that starts with “use” and has a loader, like useTexture is cached, so you’re working on the same texture. if you mutate it, all other places that rely on that texture will be affected. if you want a specific texture you clone it. though i would watch out, materials are very expensive in threejs and if you end up with hundreds of clones that will tank your performance and at that point i would think about instancing and attribute shaders. it seems like you want tiles, that could be one single drawcall and a single texture atlas.

another thing is that if you setState react 120 times per second, as you do when you call handleFrameUpdate. that can only cause problems. and there’s no reason why you would want to, all you do is forward an obvious update which you could apply right away through react so you it falls into your hands again next render and then you apply it. ideally react is near-silent, for instance when you record frames in chrome > devtools > performance. it only ever comes up if some state changes, it isn’t ever involved 120 times/sec. local state is mutated in useFrame exclusively, that’s what it’s there for. please read React Three Fiber Documentation

1 Like

Thanks for your information then i will try to fix and deal with it :smiley: