Shopify / react-native-skia

High-performance React Native Graphics using Skia

Home Page:https://shopify.github.io/react-native-skia

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Animate spritesheet

mckeny3 opened this issue · 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" }}>

<Image image={map} x={0} y={50} width={211 * 16} height={15 * 16} />

    <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}
>


);
}

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

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