Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animate spritesheet #2355

Open
mckeny3 opened this issue Apr 10, 2024 · 0 comments
Open

Animate spritesheet #2355

mckeny3 opened this issue Apr 10, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@mckeny3
Copy link

mckeny3 commented Apr 10, 2024

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


);
}

@mckeny3 mckeny3 added the enhancement New feature or request label Apr 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant