Rebuilding Isometric World
Tags: react, webdev, javascript, gamedev
Published: 2023-10-09 20:59:11

---
title: Rebuilding Isometric World
published: true
date: 2023-10-09 20:53:20 UTC
tags: react,webdevelopment,javascript,gamedevelopment
canonical_url: https://medium.com/@alexsheunglaili/rebuilding-isometric-world-06333d5bcf6a
---

If you’ve been following my articles for a while, you would have known I have written a [little isometric grid demo](https://github.com/sheunglaili/isometric-grid) in pure Javascript.

![Isometric grid demo](https://cdn-images-1.medium.com/max/600/1*UBfuFATNU8ZJGR-ziKig4Q.gif)

That approach works well for what I was trying to archive but I am planning on adding more functionality into the website. Hence in this article, let me rebuild the project using [Pixi.js](https://pixijs.com/) and it’s React binding, [React Pixi](https://pixijs.io/pixi-react/).

> If you wanna learn more about the math behind isometric graphics, I suggests you checking out [this video](https://www.youtube.com/watch?v=04oQ2jOUjkU) for it’s in depth explanation

![Assets pack](https://cdn-images-1.medium.com/max/347/1*fPrJBAIfY7kAIBnyKeW5Mg.png)
_Awesome asset pack from @dani_maccari !_

For starters, I found a great isometric asset pack from [itch.io](https://dani-maccari.itch.io/tiny-blocks-isometric-pixel-assets) that suits my needs. It includes some fine detailed blocks which would come in handy in some of the future use cases!

Secondly, let’s set up a react project using create-react-app and install the dependencies we need:

```shell
npx create-react-app isometric-world --template typescript

cd isometric-world && npm install pixi.js @pixi/react
```

Now we have our project setup, let’s unzip the assets and place it into the public directory in your project. Doing so allows pixi.js to reference the assets as [sprite sheets](https://pixijs.com/guides/components/sprite-sheets) in later step.

Following React-Pixi Getting Started tutorial, we should have the stage setup like this:

```tsx
import { BlurFilter } from 'pixi.js';
import { Stage, Container, Sprite, Text } from '@pixi/react';
import { useMemo } from 'react';

export const MyComponent = () =>
{
  const blurFilter = useMemo(() => new BlurFilter(4), []);

  return (
    <Stage>
      <Sprite
        image="https://pixijs.io/pixi-react/img/bunny.png"
        x={400}
        y={270}
        anchor={{ x: 0.5, y: 0.5 }}
      />

      <Container x={400} y={330}>
        <Text text="Hello World" anchor={{ x: 0.5, y: 0.5 }} filters={[blurFilter]} />
      </Container>
    </Stage>
  );
};
```

Let’s prune some of the codes we don’t need and update the Stage to fill up the available space:

```tsx
export function App(){
  return (
    <Stage 
      height={window.innerHeight} // set initial height to window.innerHeight
      width={window.innerWidth} // set initial width to window.innerWidth
      options={{ resizeTo: window }} // resize to window whenever size changes
    >
    </Stage>
  );
};
```

With the stage ready, it’s time to bring the blocks in! As mentioned, I am gonna load the sprites with [Sprite Sheets](https://pixijs.download/dev/docs/PIXI.Spritesheet.html). The sprite sheets from the Tinyblock assets pack are in 18 pixels x 18 pixels, there are total of 10 rows & 10 columns of block which makes the size of the sprite sheets 180pixels x 180pixels!

```tsx
const atlasData = {
  frames: {
    pale_green_grass: {
      // the location of the block
      // located at top left corner with (0, 0) 
      // with a size of 18 x 18 pixels 
      frame: { x: 0, y: 0, w: 18, h: 18 },
      sourceSize: { w: 18, h: 18 },
      spriteSourceSize: { x: 0, y: 0, w: 18, h: 18 }
    },
  },
  meta: {
    // renamed one of the sprite sheets into `structural_blocks.png`
    // and placed it in `public/sprites`
    image: `${process.env.PUBLIC_URL}/sprites/structural_blocks.png`,
    format: 'RGBA8888',
    size: { w: 180, h: 180 },
    // scale is 1:1
    scale: "1",
  },
};

export function App(){
  const [textures, setTextures] = useState<utils.Dict<Texture<Resource>>>();
  useEffect(() => {
    const spritesheet = new Spritesheet(
      BaseTexture.from(atlasData.meta.image, { scaleMode: SCALE_MODES.NEAREST }),
      atlasData
    );
    spritesheet.parse().then(setTextures)
  }, []);

  // ... render bits
};
```

Loading Sprite Sheets is an async operation, we have to useState and set the resulting textures into state.

To draw the resulting texture onto the screen, we need to wire the texture into the [sprite](https://pixijs.io/pixi-react/components/Sprite/).

```tsx
function App() {

  const [textures, setTextures] = useState<utils.Dict<Texture<Resource>>>();
  useEffect(() => {
    const spritesheet = new Spritesheet(
      BaseTexture.from(atlasData.meta.image, { scaleMode: SCALE_MODES.NEAREST }),
      atlasData
    );
    spritesheet.parse().then(setTextures)
  }, []);

  return (
    <Stage
      height={window.innerHeight}
      width={window.innerWidth}
      options={{ resizeTo: window }}>
      {textures
        ? <Sprite
            texture={textures['pale_green_grass']}
            scale={4} // scale into 4x
            anchor={{ x: 0.5, y: 0.5 }} // the anchor point would be center of the sprite
            x={36} // 72 / 2 = 36 so sprite is aligned in the middle
            y={36} 
          /> 
        : null
      }
    </Stage>
  );
}
```

With everything setup properly, this should appear in your browser window!

![tiny grass block](https://cdn-images-1.medium.com/max/1024/1*jQPmuWAz6x7zIBxIK8T5jQ.png)
_A tiny grass block yeh!!!_

Looking back at my original repository, I have them line up in an isometric fashion with 16 x 16 pattern. Let’s apply the same principal in here. To do so, I wrote some utils functions that would come in handy

```tsx
import { matrix, multiply } from "mathjs";

type Block = {
    x: number
    y: number
}

export function screen_to_isometric(x: number, y: number) {
    // the weights as explanined in the youtube video
    const isometric_weights = matrix([
        [0.5, 0.25],
        [-0.5, 0.25]
    ]);

    // coordinatex times the size of the block 18 * 4 = 72
    // as it's scaled 4x
    const coordinate = matrix([
        [x * 72, y * 72]
    ]);

    const [isometricCoordinate] = multiply(coordinate, isometricWeights).toArray();
    return isometricCoordinate as number[];
}

export function generateGrid<T extends Block = Block>(
    rows: number, 
    cols: number, 
    blockProvider: (x: number, y: number) => T) {
    const grid: T[] = [];
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            grid.push(blockProvider(i, j));
        }
    }
    return grid;
}

function App() {

  // ... texture loading code

  // generate the grid using utils method
  const grid = useMemo(() => generateGrid(16, 16, (x, y) => ({ x, y })), [])

  return (
    <Stage
      height={window.innerHeight}
      width={window.innerWidth}
      options={{ resizeTo: window }}>
      {textures
        ? grid.map(({ x, y }) => {
          // convert the screen coordinate to isometric coordinate
          const [isometric_x, isometric_y] = screen_to_isometric(x , y);
          return (
            <Sprite
              texture={textures["pale_green_grass"]}
              x={isometric_x + window.innerWidth / 2} // center horizontally
              y={isometric_y + window.innerHeight / 4} // align the y axis to one fourth of the screen
              anchor={{ x: 0, y: 0 }}
            />
          )
        })
        : null
      }
    </Stage>
  );
}
```

A perfect green plane should now appear on your screen!

![more grass blocks](https://cdn-images-1.medium.com/max/1024/1*df7WznYZn3qeZ2Q2JazcXQ.png)
_we are very close to replicating our existing website!_

The next step would be to replicate the wave motion as seen on the top of the article. To do that, we would need to [useTick hook](https://pixijs.io/pixi-react/hooks/#usetick) from React-Pixi to update the y-axis of corresponding block on each animation frame. In order to do that, we have to extract the sprite into it’s own component.

```tsx
type GrassBlockProps = {
  x: number
  y: number
  texture: Texture<Resource>
}

function GrassBlock({ x, y, texture }: GrassBlockProps) {
  const [isometric_x, isometric_y] = useMemo(() => screen_to_isometric(x, y), [x, y])
  const [elevation, setElevation] = useState(0);
  const [timeElapsed, setTimeElapsed] = useState(0);
  useTick((_, ticker) => {
    // save elapsed time
    setTimeElapsed(timeElapsed + ticker.deltaMS / 500);
    // apply cosine function to elapsed time to create a wave form
    setElevation(16 * Math.cos(timeElapsed - x))
  });
  return (
    <Sprite
      texture={texture}
      x={isometric_x + window.innerWidth / 2}
      y={isometric_y + window.innerHeight / 4 + elevation}
      anchor={{ x: 0, y: 0 }}
    />
  )
}
```

And update the stage with the new component

```tsx
function App() {

  //... 

  return (
    <Stage
      height={window.innerHeight}
      width={window.innerWidth}
      options={{ resizeTo: window }}>
      {textures
        ? grid.map(({ x, y }) => (
          <GrassBlock
            x={x}
            y={y}
            texture={textures["pale_green_grass"]}
          />
        ))
        : null
      }
    </Stage>
  );
}
```

Ta da! There you have it isometric grid replicated with pixi.js + React pixi.

![final product](https://cdn-images-1.medium.com/max/600/1*W1RQL9vu0BEFa_zwYNG5ew.gif)

Like I mentioned this is going to be the end of the conversion from vanilla JS to Pixi.js. I hope you enjoyed the content and find this interesting!

I am also [open for hire on Fiverr](https://www.fiverr.com/shlaili)! If you like what you see and think my skills is what you need, feel free to send me a message there or on my Twitter (X): [https://twitter.com/sheunglaili](https://twitter.com/sheunglaili)!

Thanks again for watching!