react-native-skia icon indicating copy to clipboard operation
react-native-skia copied to clipboard

Animate spritesheet

Open mckeny3 opened this issue 1 year ago • 1 comments

Uploading 20240410_144737.mp4…

Description

Hello @william-candillon,

I've noticed several discussions in the community where developers, including myself, were unsure about how to properly animate a sprite sheet using React Native Skia. After much experimentation and navigating through a bit of trial and error, I've developed a solution that seems to work effectively.

Given the apparent gap in resources and tutorials on this topic, I was wondering if you might consider creating a dedicated component to simplify this process for others. Alternatively, a tutorial video on your YouTube channel could greatly benefit the community by providing a clear, accessible guide on sprite sheet animation with React Native Skia.

Thank you for considering this suggestion. Your contributions have been incredibly valuable to the community, and I believe this could be another great addition.

import { MaterialCommunityIcons } from "@expo/vector-icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Pressable, View, useWindowDimensions } from "react-native"; import { collisions } from "../data/collision"; import { PlatformConfig } from "@/Store/PlatformStore"; import { is2DColiding, isColiding } from "@/helpers/isColiding";

import { Canvas, Circle, Group, Image, Mask, Path, Rect, useClock, useImage, vec, } from "@shopify/react-native-skia"; import { SharedValue, useDerivedValue, useFrameCallback, useSharedValue, } from "react-native-reanimated"; export default function App() { const [newTileMap, setNewTileMap] = useState<PlatformConfig[]>([]); const vec2 = (x: SharedValue, y: SharedValue) => { return { x, y }; };

const tileSize = 32; const tileRows = 211; const tileCols = 15; const SCREEN_WIDTH = useWindowDimensions().width; const SCREEN_HEIGHT = useWindowDimensions().height; const mario_spriteSheet = useImage(require("../assets/sprites/mario_spritesheet.png")); const spriteWidth = 186; const spriteHeight = 34; const frameSize = vec(spriteWidth / 6, spriteHeight);

enum PlayerState { IDLE, WALKING, JUMPING, FALLING, }

const playerState = useSharedValue(PlayerState.IDLE); const bg = useImage(require("../assets/sprites/tiles/bg.png")); const wall = useImage(require("../assets/sprites/tiles/land_14.png")); const map = useImage(require("../assets/sprites/tiles/map.png")); const direction = { left: useSharedValue(false), right: useSharedValue(false), up: useSharedValue(false), down: useSharedValue(false), };

const groundY = SCREEN_HEIGHT - 240;

const loadMap = useCallback(() => { const tileMap: PlatformConfig[] = []; collisions.forEach((row, rowIndex) => { row.forEach((col, colIndex) => { if (col !== 0) { tileMap.push({ x: colIndex * tileSize, y: rowIndex * tileSize, w: tileSize, h: tileSize, val: col, }); } }); }); setNewTileMap(tileMap); }, []);

useEffect(() => { loadMap(); }, []);

const player = { h: frameSize.y, w: frameSize.x, velocity: vec2(useSharedValue(0), useSharedValue(0)), pos: { x: useSharedValue(250), y: useSharedValue(groundY + 154), }, };

const world = { x: useSharedValue(0), y: useSharedValue(groundY), vel: vec2(useSharedValue(0), useSharedValue(0)), };

const transform = useDerivedValue(() => { return [ { translateX: world.x.value }, { translateY: world.y.value }, { scaleX: 2 }, { scaleY: 2 } ]; });

const frame = useSharedValue(1); const frameDerived = useDerivedValue(() => { return -frame.value * frameSize.x; }); const elapsed = useSharedValue(0);

const prevWorldPosX = useSharedValue(0); const prevWorldPosY = useSharedValue(0);

const getTileX = (x: number) => { 'worklet'

return x + world.x.value;

} const getTileY = (y: number) => { 'worklet'

return y + world.y.value + groundY;

}

const playerOBJ = { x: player.pos.x.value, y: player.pos.y.value, w: frameSize.x, h: frameSize.y, };

function applyGravity(){ 'worklet' world.vel.y.value -= 2; world.y.value += world.vel.y.value; };

function checkCollisionv(){ 'worklet' newTileMap.forEach((tile) => { const adjustedTile = { x: getTileX(tile.x), y: getTileY(tile.y - 45), w: tile.w, h: tile.h, };

  if (is2DColiding(adjustedTile, playerOBJ)) {
    if (world.vel.y.value < 0) {
      let overlap = playerOBJ.y + playerOBJ.h - adjustedTile.y;
      world.y.value += overlap + 0.01;
      world.vel.y.value = 0;
    } else if (world.vel.y.value > 0) {
      world.vel.y.value = 0;
      world.y.value -= adjustedTile.y + adjustedTile.h - playerOBJ.y - 0.01;
    }
  }
});

};

function checkCollisionH(){ 'worklet' newTileMap.forEach((tile) => { const adjustedTile = { x: getTileX(tile.x), y: getTileY(tile.y), w: tile.w, h: tile.h, };

  if (is2DColiding(adjustedTile, playerOBJ)) {
    if (world.vel.x.value < 0) {
      const overlap = playerOBJ.x + playerOBJ.w - adjustedTile.x;
      world.x.value += overlap + 0.01;
      world.vel.x.value = 0;
    } else if (world.vel.x.value > 0) {
      world.x.value -= adjustedTile.x + adjustedTile.w - playerOBJ.x + 0.01;
      world.vel.x.value = 0;
    }
  }
});

}; useFrameCallback(({ timeSincePreviousFrame: dt }) => { if (!dt) return;

prevWorldPosX.value = world.x.value;
prevWorldPosY.value = world.y.value;
world.x.value += world.vel.x.value;

elapsed.value += dt;
if (playerState.value === PlayerState.WALKING) {
  frame.value = Math.floor(elapsed.value / 0.6) % 2;
} else if (playerState.value === PlayerState.IDLE) {
  frame.value = 4;
} else if (playerState.value === PlayerState.JUMPING) {
  frame.value = 5;
} else if (playerState.value === PlayerState.FALLING) {
  frame.value = 3;
}

function checkPlayerStateAndVelocity(){ 'worklet' if (direction.right.value) { playerState.value = PlayerState.WALKING; world.vel.x.value = -3; } else if (direction.left.value) { playerState.value = PlayerState.WALKING; world.vel.x.value = 3; } else if (!direction.up.value && !direction.down.value && !direction.left.value && !direction.right.value) { playerState.value = PlayerState.IDLE; }

if (direction.up.value) {
  playerState.value = PlayerState.JUMPING;
  world.vel.y.value = 15;
} else if (direction.down.value) {
  playerState.value = PlayerState.FALLING;
  world.vel.y.value = 3;
} else {
  world.vel.y.value = 0;
}

};

checkPlayerStateAndVelocity(); applyGravity(); checkCollisionv(); checkCollisionH();

});

return ( <View style={{ position: "relative", flex: 1, justifyContent: "center", alignItems: "center" }}> <Canvas style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT, backgroundColor: "#5c94fc" }}> <Group transform={transform}> <Image image={map} x={0} y={50} width={211 * 16} height={15 * 16} /> </Group>

    <Group transform={transform}>
      {newTileMap.map((tile, index) => (
        <Image
          key={index}
          opacity={0.5}
          image={tile.val === 1 ? wall : bg}
          x={tile.x}
          y={tile.y - 160}
          width={tile.w}
          height={tile.h}
        />
      ))}
    </Group>

    <Group
      origin={{ y: SCREEN_HEIGHT, x: 0 }}
      transform={[
        { translateX: player.pos.x.value },
        { translateY: player.pos.y.value },
      ]}
    >
      <Mask mask={<Rect x={0} y={0} width={frameSize.x} height={frameSize.y} />}>
        <Image
          image={mario_spriteSheet}
          x={frameDerived}
          y={0}
          width={spriteWidth}
          height={spriteHeight}
        />
      </Mask>
    </Group>
  </Canvas>

  {createControlButton("arrow-left", 60, () => {
    direction.left.value = true;
  }, () => {
    direction.left.value = false;
    if (playerState.value === PlayerState.WALKING) {
      world.vel.x.value = 0;
    }
  })}

  {createControlButton("arrow-right", 20, () => {
    direction.right.value = true;
  }, () => {
    direction.right.value = false;
    if (playerState.value === PlayerState.WALKING) {
      world.vel.x.value = 0;
    }
  })}

  {createControlButton("arrow-up", 100, () => {
    direction.up.value = true;
  }, () => {
    direction.up.value = false;
  })}
</View>

); }

function createControlButton(iconName: string|any, positionLeft: number, onPressIn: () => void, onPressOut: () => void): JSX.Element { return ( <Pressable style={{ position: "absolute", bottom: 20, left: positionLeft, }} onPressIn={onPressIn} onPressOut={onPressOut} > <MaterialCommunityIcons name={iconName} size={24} color="red" /> </Pressable> ); }

mckeny3 avatar Apr 10 '24 19:04 mckeny3

Can you please formate your code and may be upload the video for more details? It will be very helpful.

imVinayPandya avatar May 20 '24 09:05 imVinayPandya

Please let know if you have a reproducible example and what you think the bug might be.

wcandillon avatar May 28 '24 14:05 wcandillon