├── README.md
├── performance
└── offcanvas-drawing
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ ├── assets
│ │ └── tiles.png
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ ├── src
│ ├── App.css
│ ├── App.js
│ ├── classes
│ │ ├── camera.js
│ │ ├── game.js
│ │ ├── keyboard.js
│ │ ├── loader.js
│ │ └── tileMap.js
│ ├── index.css
│ └── index.js
│ └── yarn.lock
└── square
├── collisions-in-tilemaps
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── assets
│ │ ├── character.png
│ │ └── tiles.png
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── classes
│ │ ├── camera.js
│ │ ├── game.js
│ │ ├── hero.js
│ │ ├── keyboard.js
│ │ ├── loader.js
│ │ └── tileMap.js
│ ├── index.css
│ └── index.js
└── yarn.lock
├── non-scrolling-tilemaps
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── assets
│ │ └── tiles.png
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── classes
│ │ ├── game.js
│ │ ├── loader.js
│ │ └── tileMap.js
│ ├── index.css
│ └── index.js
└── yarn.lock
├── scrolling-tilemaps
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── assets
│ │ └── tiles.png
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── classes
│ │ ├── camera.js
│ │ ├── game.js
│ │ ├── keyboard.js
│ │ ├── loader.js
│ │ └── tileMap.js
│ ├── index.css
│ └── index.js
└── yarn.lock
└── tilemaps-with-layers
├── .gitignore
├── README.md
├── package.json
├── public
├── assets
│ ├── character.png
│ └── tiles.png
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── classes
│ ├── game.js
│ ├── loader.js
│ └── tileMap.js
├── index.css
└── index.js
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # Tile maps examples with React
2 | This is a collection of demos and examples on how to use and implement **tilemaps** in **React**.
3 |
4 | 
5 |
6 | ## Setup
7 |
8 | - Clone this repo
9 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
10 |
11 | ## Demos
12 | #### Square Tiles
13 | 1. [Non-scrolling tilemaps](https://github.com/fatihturgut/react-tilemaps-examples/tree/master/square/non-scrolling-tilemaps)
14 | 2. [Tilemaps with layers](https://github.com/fatihturgut/react-tilemaps-examples/tree/master/square/tilemaps-with-layers)
15 | 3. [Scrolling tilemaps](https://github.com/fatihturgut/react-tilemaps-examples/tree/master/square/scrolling-tilemaps)
16 | 4. [Collisions in tilemaps](https://github.com/fatihturgut/react-tilemaps-examples/tree/master/square/collisions-in-tilemaps)
17 |
18 | #### Performance improvements
19 | 1. [Offcanvas drawing](https://github.com/fatihturgut/react-tilemaps-examples/tree/master/performance/offcanvas-drawing)
20 |
21 | ## References
22 | 1. [Mozilla Tilemaps Examples](http://mozdevs.github.io/gamedev-js-tiles/)
23 | 2. [Mozilla Articles about Tiles and tilemaps overview](https://developer.mozilla.org/en-US/docs/Games/Techniques/Tilemaps)
24 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/README.md:
--------------------------------------------------------------------------------
1 | # Offcanvas Drawing (scrolling maps)
2 |
3 | 
4 |
5 | ### Setup
6 |
7 | - Clone this repo
8 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
9 |
10 | - Go to project directory
11 | `cd react-tilemaps-examples/performance/offcanvas-drawing`
12 |
13 | - Install npm packages with yarn
14 | `yarn`
15 |
16 | ### Usage
17 |
18 | - Start the app on localhost:3000 !
19 | `yarn start`
20 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "offcanvas-drawing",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.0"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/public/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/performance/offcanvas-drawing/public/assets/tiles.png
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/performance/offcanvas-drawing/public/favicon.ico
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Source Code Pro', monospace;
3 | }
4 |
5 | .header {
6 | height: 50px;
7 | padding: 10px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #31ffd5;
12 | font-size: 30px;
13 | font-weight: 600;
14 | }
15 |
16 | .subheader {
17 | height: 20px;
18 | padding: 10px;
19 | padding-top: 20px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | }
24 |
25 | .subheader2 {
26 | height: 20px;
27 | padding-bottom: 30px;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
33 | .gameContainer {
34 | height: 100%;
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | background-color: #ffffff;
39 | }
40 |
41 | .flex {
42 | padding: 10;
43 | margin-top: 20px;
44 | justify-content: center;
45 | align-items: center;
46 | display: flex;
47 | }
48 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Game from './classes/game';
3 | import "./App.css";
4 |
5 | const CANVAS_WIDTH = 512;
6 | const CANVAS_HEIGHT = 512;
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | isGameRunning: false
13 | };
14 | this.canvasRef = React.createRef();
15 | }
16 |
17 | componentDidMount = () => {
18 | this.start();
19 | };
20 |
21 | start = async () => {
22 | if (!this.state.isGameRunning) {
23 | this.game = new Game(this.getContext());
24 | await this.game.init();
25 | this.renderGame();
26 | }
27 | this.setState(state => ({ isGameRunning: !state.isGameRunning }));
28 | };
29 |
30 | renderGame = () => {
31 | requestAnimationFrame((elapsed) => {
32 | this.game.render(elapsed);
33 |
34 | if (this.state.isGameRunning) {
35 | this.renderGame();
36 | }
37 | });
38 | };
39 |
40 | getContext = () => this.canvasRef.current.getContext("2d");
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Tilemaps examples (with React)
47 |
48 |
49 | Offcanvas drawing
50 |
51 |
52 | Use arrow keys to move
53 |
54 |
55 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/classes/camera.js:
--------------------------------------------------------------------------------
1 | export default class Camera {
2 | constructor(map, width, height) {
3 | this.x = 0;
4 | this.y = 0;
5 | this.width = width;
6 | this.height = height;
7 | this.maxX = map.columns * map.tileSize - width;
8 | this.maxY = map.rows * map.tileSize - height;
9 | this.SPEED = 256; // pixels per second
10 | }
11 |
12 | move = (delta, dirX, dirY) => {
13 | // move camera
14 | this.x += dirX * this.SPEED * delta;
15 | this.y += dirY * this.SPEED * delta;
16 | // clamp values
17 | this.x = Math.max(0, Math.min(this.x, this.maxX));
18 | this.y = Math.max(0, Math.min(this.y, this.maxY));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/classes/game.js:
--------------------------------------------------------------------------------
1 | import Loader from "./loader";
2 | import TileMap from "./tileMap";
3 | import Camera from "./camera";
4 | import Keyboard from "./keyboard";
5 |
6 | export default class Game {
7 | constructor(context) {
8 | this.context = context;
9 | this.loader = new Loader();
10 | this.map = new TileMap();
11 | this.camera = new Camera(this.map, 512, 512);
12 | this.keyboard = new Keyboard();
13 | this._previousElapsed = 0;
14 | }
15 |
16 | init = async () => {
17 | this.keyboard.listenForEvents(
18 | [this.keyboard.LEFT, this.keyboard.RIGHT, this.keyboard.UP, this.keyboard.DOWN]);
19 | const tiles = await this.loader.loadImage("tiles", "./assets/tiles.png");
20 | this.tileAtlas = this.loader.getImage("tiles");
21 | this.images = {
22 | tiles,
23 | };
24 |
25 | this.layerCanvas = this.map.layers.map(function () {
26 | const canvas = document.createElement('canvas');
27 | canvas.width = 512;
28 | canvas.height = 512;
29 | return canvas;
30 | });
31 |
32 | // initial draw of the map
33 | this._drawMap();
34 | };
35 |
36 | _drawMap = () => {
37 | this.map.layers.forEach((layer, layerIndex) => {
38 | this.drawLayer(layerIndex);
39 | });
40 | };
41 |
42 | drawLayer = layerIndex => {
43 | const context = this.layerCanvas[layerIndex].getContext('2d');
44 | context.clearRect(0, 0, 512, 512);
45 | const startColumn = Math.floor(this.camera.x / this.map.tileSize);
46 | const endColumn = startColumn + (this.camera.width / this.map.tileSize);
47 | const startRow = Math.floor(this.camera.y / this.map.tileSize);
48 | const endRow = startRow + (this.camera.height / this.map.tileSize);
49 | const offsetX = -this.camera.x + startColumn * this.map.tileSize;
50 | const offsetY = -this.camera.y + startRow * this.map.tileSize;
51 |
52 | for (let columnIndex = startColumn; columnIndex < endColumn; columnIndex++) {
53 | for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
54 | let tile = this.map.getTile(layerIndex, columnIndex, rowIndex);
55 | const x = (columnIndex - startColumn) * this.map.tileSize + offsetX;
56 | const y = (rowIndex - startRow) * this.map.tileSize + offsetY;
57 | if (tile !== 0) { // 0 => empty tile
58 | context.drawImage(
59 | this.tileAtlas, // image
60 | (tile - 1) * this.map.tileSize, // source x
61 | 0, // source y
62 | this.map.tileSize, // source width
63 | this.map.tileSize, // source height
64 | Math.round(x), // target x
65 | Math.round(y), // target y
66 | this.map.tileSize, // target width
67 | this.map.tileSize // target height
68 | );
69 | }
70 | }
71 | }
72 | };
73 |
74 | update = delta => {
75 | this.hasScrolled = false;
76 | // handle camera movement with arrow keys
77 | let dirX = 0;
78 | let dirY = 0;
79 | if (this.keyboard.isDown(this.keyboard.LEFT)) { dirX = -1; }
80 | if (this.keyboard.isDown(this.keyboard.RIGHT)) { dirX = 1; }
81 | if (this.keyboard.isDown(this.keyboard.UP)) { dirY = -1; }
82 | if (this.keyboard.isDown(this.keyboard.DOWN)) { dirY = 1; }
83 |
84 | if (dirX !== 0 || dirY !== 0) {
85 | this.camera.move(delta, dirX, dirY);
86 | this.hasScrolled = true;
87 | }
88 | };
89 |
90 | getDelta = elapsed => {
91 | // compute delta time in seconds -- also cap it
92 | let delta = (elapsed - this._previousElapsed) / 1000.0;
93 | delta = Math.min(delta, 0.25); // maximum delta of 250 ms
94 | this._previousElapsed = elapsed;
95 | return delta;
96 | }
97 |
98 | render(elapsed) {
99 | this.context.clearRect(0, 0, 512, 512);
100 | this.update(this.getDelta(elapsed));
101 | // re-draw map if there has been scroll
102 | if (this.hasScrolled) {
103 | this._drawMap();
104 | }
105 |
106 | // draw the map layers into game context
107 | this.context.drawImage(this.layerCanvas[0], 0, 0);
108 | this.context.drawImage(this.layerCanvas[1], 0, 0);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/classes/keyboard.js:
--------------------------------------------------------------------------------
1 | export default class Keyboard {
2 | constructor() {
3 | this.LEFT = 37;
4 | this.RIGHT = 39;
5 | this.UP = 38;
6 | this.DOWN = 40;
7 | this._keys = {};
8 | }
9 |
10 | listenForEvents = keys => {
11 | window.addEventListener('keydown', this._onKeyDown);
12 | window.addEventListener('keyup', this._onKeyUp);
13 |
14 | keys.forEach(key => {
15 | this._keys[key] = false;
16 | });
17 | }
18 |
19 | _onKeyDown = event => {
20 | const keyCode = event.keyCode;
21 | if (keyCode in this._keys) {
22 | event.preventDefault();
23 | this._keys[keyCode] = true;
24 | }
25 | };
26 |
27 | _onKeyUp = event => {
28 | const keyCode = event.keyCode;
29 | if (keyCode in this._keys) {
30 | event.preventDefault();
31 | this._keys[keyCode] = false;
32 | }
33 | };
34 |
35 | isDown = keyCode => {
36 | if (!keyCode in this._keys) {
37 | throw new Error(`Keycode ${keyCode} is not being listened to`);
38 | }
39 | return this._keys[keyCode];
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/classes/loader.js:
--------------------------------------------------------------------------------
1 | export default class Loader {
2 | constructor() {
3 | this.images = {};
4 | }
5 |
6 | loadImage = (key, src) => {
7 | const image = new Image();
8 | const promise = new Promise((resolve, reject) => {
9 | image.onload = () => {
10 | this.images[key] = image;
11 | resolve(image);
12 | };
13 |
14 | image.onerror = () => {
15 | reject("Could not load image: " + src);
16 | };
17 | });
18 |
19 | image.src = src;
20 | return promise;
21 | };
22 |
23 | getImage = key => {
24 | return key in this.images ? this.images[key] : null;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/classes/tileMap.js:
--------------------------------------------------------------------------------
1 | export default class TileMap {
2 | constructor() {
3 | this.columns = 12;
4 | this.rows = 12;
5 | this.tileSize = 64;
6 | this.layers = [[
7 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
8 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
9 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
10 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
11 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
12 | 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
13 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
14 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
15 | 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3,
16 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
17 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
18 | 3, 3, 3, 1, 1, 2, 3, 3, 3, 3, 3, 3
19 | ], [
20 | 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
21 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
22 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
23 | 4, 0, 0, 5, 0, 0, 0, 0, 0, 5, 0, 4,
24 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
25 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
26 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
27 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
28 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
29 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
30 | 4, 4, 4, 0, 5, 4, 4, 4, 4, 4, 4, 4,
31 | 4, 4, 4, 0, 0, 3, 3, 3, 3, 3, 3, 3
32 | ]];
33 | }
34 |
35 | getTile(layerIndex, columnIndex, rowIndex) {
36 | return this.layers[layerIndex][rowIndex * this.columns + columnIndex];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/performance/offcanvas-drawing/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/README.md:
--------------------------------------------------------------------------------
1 | # Collisions in tilemaps
2 |
3 | 
4 |
5 | ### Setup
6 |
7 | - Clone this repo
8 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
9 |
10 | - Go to project directory
11 | `cd react-tilemaps-examples/square/collisions-in-tilemaps`
12 |
13 | - Install npm packages with yarn
14 | `yarn`
15 |
16 | ### Usage
17 |
18 | - Start the app on localhost:3000 !
19 | `yarn start`
20 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "collisions-in-tilemaps",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.0"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/public/assets/character.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/collisions-in-tilemaps/public/assets/character.png
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/public/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/collisions-in-tilemaps/public/assets/tiles.png
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/collisions-in-tilemaps/public/favicon.ico
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Source Code Pro', monospace;
3 | }
4 |
5 | .header {
6 | height: 50px;
7 | padding: 10px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #31ffd5;
12 | font-size: 30px;
13 | font-weight: 600;
14 | }
15 |
16 | .subheader {
17 | height: 20px;
18 | padding: 10px;
19 | padding-top: 20px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | }
24 |
25 | .subheader2 {
26 | height: 20px;
27 | padding-bottom: 30px;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
33 | .gameContainer {
34 | height: 100%;
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | background-color: #ffffff;
39 | }
40 |
41 | .flex {
42 | padding: 10;
43 | margin-top: 20px;
44 | justify-content: center;
45 | align-items: center;
46 | display: flex;
47 | }
48 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Game from './classes/game';
3 | import "./App.css";
4 |
5 | const CANVAS_WIDTH = 512;
6 | const CANVAS_HEIGHT = 512;
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | isGameRunning: false
13 | };
14 | this.canvasRef = React.createRef();
15 | }
16 |
17 | componentDidMount = () => {
18 | this.start();
19 | };
20 |
21 | start = async () => {
22 | if (!this.state.isGameRunning) {
23 | this.game = new Game(this.getContext());
24 | await this.game.init();
25 | this.renderGame();
26 | }
27 | this.setState(state => ({ isGameRunning: !state.isGameRunning }));
28 | };
29 |
30 | renderGame = () => {
31 | requestAnimationFrame((elapsed) => {
32 | this.game.render(elapsed);
33 |
34 | if (this.state.isGameRunning) {
35 | this.renderGame();
36 | }
37 | });
38 | };
39 |
40 | getContext = () => this.canvasRef.current.getContext("2d");
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Tilemaps examples (with React)
47 |
48 |
49 | Collisions
50 |
51 |
52 | Use arrow keys to move
53 |
54 |
55 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/camera.js:
--------------------------------------------------------------------------------
1 | export default class Camera {
2 | constructor(map, width, height) {
3 | this.x = 0;
4 | this.y = 0;
5 | this.width = width;
6 | this.height = height;
7 | this.maxX = map.columns * map.tileSize - width;
8 | this.maxY = map.rows * map.tileSize - height;
9 | }
10 |
11 | follow = sprite => {
12 | this.following = sprite;
13 | sprite.screenX = 0;
14 | sprite.screenY = 0;
15 | }
16 |
17 | update = () => {
18 | // assume followed sprite should be placed at the center of the screen
19 | // whenever possible
20 | this.following.screenX = this.width / 2;
21 | this.following.screenY = this.height / 2;
22 |
23 | // make the camera follow the sprite
24 | this.x = this.following.x - this.width / 2;
25 | this.y = this.following.y - this.height / 2;
26 | // clamp values
27 | this.x = Math.max(0, Math.min(this.x, this.maxX));
28 | this.y = Math.max(0, Math.min(this.y, this.maxY));
29 |
30 | // in map corners, the sprite cannot be placed in the center of the screen
31 | // and we have to change its screen coordinates
32 |
33 | // left and right sides
34 | if (this.following.x < this.width / 2 ||
35 | this.following.x > this.maxX + this.width / 2) {
36 | this.following.screenX = this.following.x - this.x;
37 | }
38 | // top and bottom sides
39 | if (this.following.y < this.height / 2 ||
40 | this.following.y > this.maxY + this.height / 2) {
41 | this.following.screenY = this.following.y - this.y;
42 | }
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/game.js:
--------------------------------------------------------------------------------
1 | import Loader from "./loader";
2 | import TileMap from "./tileMap";
3 | import Camera from "./camera";
4 | import Keyboard from "./keyboard";
5 | import Hero from "./hero";
6 |
7 | export default class Game {
8 | constructor(context) {
9 | this.context = context;
10 | this.loader = new Loader();
11 | this.map = new TileMap();
12 | this.keyboard = new Keyboard();
13 | this._previousElapsed = 0;
14 | }
15 |
16 | init = async () => {
17 | this.keyboard.listenForEvents(
18 | [this.keyboard.LEFT, this.keyboard.RIGHT, this.keyboard.UP, this.keyboard.DOWN]);
19 | const tiles = await this.loader.loadImage("tiles", "./assets/tiles.png");
20 | const character = await this.loader.loadImage("character", "./assets/character.png");
21 | this.tileAtlas = this.loader.getImage("tiles");
22 | this.images = {
23 | tiles,
24 | character,
25 | };
26 | this.hero = new Hero(this.map, 160, 160, this.images.character);
27 | this.camera = new Camera(this.map, 512, 512);
28 | this.camera.follow(this.hero);
29 | };
30 |
31 | drawLayer = layerIndex => {
32 | const startColumn = Math.floor(this.camera.x / this.map.tileSize);
33 | const endColumn = startColumn + (this.camera.width / this.map.tileSize);
34 | const startRow = Math.floor(this.camera.y / this.map.tileSize);
35 | const endRow = startRow + (this.camera.height / this.map.tileSize);
36 | const offsetX = -this.camera.x + startColumn * this.map.tileSize;
37 | const offsetY = -this.camera.y + startRow * this.map.tileSize;
38 |
39 | for (let columnIndex = startColumn; columnIndex < endColumn; columnIndex++) {
40 | for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
41 | let tile = this.map.getTile(layerIndex, columnIndex, rowIndex);
42 | const x = (columnIndex - startColumn) * this.map.tileSize + offsetX;
43 | const y = (rowIndex - startRow) * this.map.tileSize + offsetY;
44 | if (tile !== 0) { // 0 => empty tile
45 | this.context.drawImage(
46 | this.tileAtlas, // image
47 | (tile - 1) * this.map.tileSize, // source x
48 | 0, // source y
49 | this.map.tileSize, // source width
50 | this.map.tileSize, // source height
51 | Math.round(x), // target x
52 | Math.round(y), // target y
53 | this.map.tileSize, // target width
54 | this.map.tileSize // target height
55 | );
56 | }
57 | }
58 | }
59 | };
60 |
61 | _drawGrid = () => {
62 | const width = this.map.columns * this.map.tileSize;
63 | const height = this.map.rows * this.map.tileSize;
64 | let x, y;
65 |
66 | for (let row = 0; row < this.map.rows; row++) {
67 | x = - this.camera.x;
68 | y = row * this.map.tileSize - this.camera.y;
69 | this.context.beginPath();
70 | this.context.moveTo(x, y);
71 | this.context.lineTo(width, y);
72 | this.context.stroke();
73 | }
74 |
75 | for (let column = 0; column < this.map.columns; column++) {
76 | x = column * this.map.tileSize - this.camera.x;
77 | y = - this.camera.y;
78 | this.context.beginPath();
79 | this.context.moveTo(x, y);
80 | this.context.lineTo(x, height);
81 | this.context.stroke();
82 | }
83 | };
84 |
85 | update = delta => {
86 | // handle hero movement with arrow keys
87 | let dirX = 0;
88 | let dirY = 0;
89 | if (this.keyboard.isDown(this.keyboard.LEFT)) { dirX = -1; }
90 | if (this.keyboard.isDown(this.keyboard.RIGHT)) { dirX = 1; }
91 | if (this.keyboard.isDown(this.keyboard.UP)) { dirY = -1; }
92 | if (this.keyboard.isDown(this.keyboard.DOWN)) { dirY = 1; }
93 |
94 | this.hero.move(delta, dirX, dirY)
95 | this.camera.update();
96 | };
97 |
98 | getDelta = elapsed => {
99 | // compute delta time in seconds -- also cap it
100 | let delta = (elapsed - this._previousElapsed) / 1000.0;
101 | delta = Math.min(delta, 0.25); // maximum delta of 250 ms
102 | this._previousElapsed = elapsed;
103 | return delta;
104 | }
105 |
106 | render(elapsed) {
107 | this.context.clearRect(0, 0, 512, 512);
108 | this.update(this.getDelta(elapsed));
109 | // draw map background layer
110 | this.drawLayer(0);
111 |
112 | // draw main character
113 | this.context.drawImage(
114 | this.hero.image,
115 | this.hero.screenX - this.hero.width / 2,
116 | this.hero.screenY - this.hero.height / 2
117 | );
118 |
119 | // draw map top layer
120 | this.drawLayer(1);
121 |
122 | this._drawGrid();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/hero.js:
--------------------------------------------------------------------------------
1 | export default class Hero {
2 | constructor(map, x, y, image) {
3 | this.map = map;
4 | this.x = x;
5 | this.y = y;
6 | this.width = map.tileSize;
7 | this.height = map.tileSize;
8 | this.image = image;
9 | this.SPEED = 256; // pixels per second
10 | }
11 |
12 | move = (delta, dirX, dirY) => {
13 | // move camera
14 | this.x += dirX * this.SPEED * delta;
15 | this.y += dirY * this.SPEED * delta;
16 |
17 | // check if we walked into a non-walkable tile
18 | this._collide(dirX, dirY);
19 |
20 | // clamp values
21 | const maxX = this.map.columns * this.map.tileSize;
22 | const maxY = this.map.rows * this.map.tileSize;
23 | this.x = Math.max(0, Math.min(this.x, maxX));
24 | this.y = Math.max(0, Math.min(this.y, maxY));
25 | }
26 |
27 | _collide = (dirX, dirY) => {
28 | let row, column;
29 | // -1 in right and bottom is because image ranges from 0..63
30 | // and not up to 64
31 | const left = this.x - this.width / 2;
32 | const right = this.x + this.width / 2 - 1;
33 | const top = this.y - this.height / 2;
34 | const bottom = this.y + this.height / 2 - 1;
35 |
36 | // check for collisions on sprite sides
37 | const collision =
38 | this.map.isSolidTileAtXY(left, top) ||
39 | this.map.isSolidTileAtXY(right, top) ||
40 | this.map.isSolidTileAtXY(right, bottom) ||
41 | this.map.isSolidTileAtXY(left, bottom);
42 | if (!collision) { return; }
43 |
44 | if (dirY > 0) {
45 | row = this.map.getRow(bottom);
46 | this.y = -this.height / 2 + this.map.getY(row);
47 | }
48 | else if (dirY < 0) {
49 | row = this.map.getRow(top);
50 | this.y = this.height / 2 + this.map.getY(row + 1);
51 | }
52 | else if (dirX > 0) {
53 | column = this.map.getColumn(right);
54 | this.x = -this.width / 2 + this.map.getX(column);
55 | }
56 | else if (dirX < 0) {
57 | column = this.map.getColumn(left);
58 | this.x = this.width / 2 + this.map.getX(column + 1);
59 | }
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/keyboard.js:
--------------------------------------------------------------------------------
1 | export default class Keyboard {
2 | constructor() {
3 | this.LEFT = 37;
4 | this.RIGHT = 39;
5 | this.UP = 38;
6 | this.DOWN = 40;
7 | this._keys = {};
8 | }
9 |
10 | listenForEvents = keys => {
11 | window.addEventListener('keydown', this._onKeyDown);
12 | window.addEventListener('keyup', this._onKeyUp);
13 |
14 | keys.forEach(key => {
15 | this._keys[key] = false;
16 | });
17 | }
18 |
19 | _onKeyDown = event => {
20 | const keyCode = event.keyCode;
21 | if (keyCode in this._keys) {
22 | event.preventDefault();
23 | this._keys[keyCode] = true;
24 | }
25 | };
26 |
27 | _onKeyUp = event => {
28 | const keyCode = event.keyCode;
29 | if (keyCode in this._keys) {
30 | event.preventDefault();
31 | this._keys[keyCode] = false;
32 | }
33 | };
34 |
35 | isDown = keyCode => {
36 | if (!keyCode in this._keys) {
37 | throw new Error(`Keycode ${keyCode} is not being listened to`);
38 | }
39 | return this._keys[keyCode];
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/loader.js:
--------------------------------------------------------------------------------
1 | export default class Loader {
2 | constructor() {
3 | this.images = {};
4 | }
5 |
6 | loadImage = (key, src) => {
7 | const image = new Image();
8 | const promise = new Promise((resolve, reject) => {
9 | image.onload = () => {
10 | this.images[key] = image;
11 | resolve(image);
12 | };
13 |
14 | image.onerror = () => {
15 | reject("Could not load image: " + src);
16 | };
17 | });
18 |
19 | image.src = src;
20 | return promise;
21 | };
22 |
23 | getImage = key => {
24 | return key in this.images ? this.images[key] : null;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/classes/tileMap.js:
--------------------------------------------------------------------------------
1 | export default class TileMap {
2 | constructor() {
3 | this.columns = 12;
4 | this.rows = 12;
5 | this.tileSize = 64;
6 | this.layers = [[
7 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
8 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
9 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
10 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
11 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
12 | 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
13 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
14 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
15 | 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3,
16 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
17 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
18 | 3, 3, 3, 1, 1, 2, 3, 3, 3, 3, 3, 3
19 | ], [
20 | 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
21 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
22 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
23 | 4, 0, 0, 5, 0, 0, 0, 0, 0, 5, 0, 4,
24 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
25 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
26 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
27 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
28 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
29 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
30 | 4, 4, 4, 0, 5, 4, 4, 4, 4, 4, 4, 4,
31 | 4, 4, 4, 0, 0, 3, 3, 3, 3, 3, 3, 3
32 | ]];
33 | }
34 |
35 | getTile = (layerIndex, columnIndex, rowIndex) => {
36 | return this.layers[layerIndex][rowIndex * this.columns + columnIndex];
37 | }
38 |
39 | isSolidTileAtXY = (x, y) => {
40 | const column = Math.floor(x / this.tileSize);
41 | const row = Math.floor(y / this.tileSize);
42 |
43 | // tiles 3 and 5 are solid -- the rest are walkable
44 | // loop through all layers and return TRUE if any tile is solid
45 | return this.layers.reduce((res, layer, layerIndex) => {
46 | const tile = this.getTile(layerIndex, column, row);
47 | const isSolid = tile === 3 || tile === 5;
48 | return res || isSolid;
49 | }, false);
50 | }
51 |
52 | getColumn = x => {
53 | return Math.floor(x / this.tileSize);
54 | }
55 |
56 | getRow = y => {
57 | return Math.floor(y / this.tileSize);
58 | }
59 |
60 | getX = column => {
61 | return column * this.tileSize;
62 | }
63 |
64 | getY = row => {
65 | return row * this.tileSize;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/square/collisions-in-tilemaps/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/README.md:
--------------------------------------------------------------------------------
1 | # Non-scrolling tilemaps
2 |
3 | 
4 |
5 | ### Setup
6 |
7 | - Clone this repo
8 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
9 |
10 | - Go to project directory
11 | `cd react-tilemaps-examples/square/non-scrolling-tilemaps`
12 |
13 | - Install npm packages with yarn
14 | `yarn`
15 |
16 | ### Usage
17 |
18 | - Start the app on localhost:3000 !
19 | `yarn start`
20 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "non-scrolling-tilemaps",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.0"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/public/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/non-scrolling-tilemaps/public/assets/tiles.png
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/non-scrolling-tilemaps/public/favicon.ico
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Source Code Pro', monospace;
3 | }
4 |
5 | .header {
6 | height: 50px;
7 | padding: 10px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #31ffd5;
12 | font-size: 30px;
13 | font-weight: 600;
14 | }
15 |
16 | .subheader {
17 | height: 20px;
18 | padding: 10px;
19 | margin-top: 20px;
20 | margin-bottom: 30px;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | }
25 |
26 | .gameContainer {
27 | height: 100%;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | background-color: #ffffff;
32 | }
33 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Game from './classes/game';
3 | import "./App.css";
4 |
5 | const CANVAS_WIDTH = 512;
6 | const CANVAS_HEIGHT = 512;
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | isGameRunning: false
13 | };
14 | this.canvasRef = React.createRef();
15 | }
16 |
17 | componentDidMount = () => {
18 | this.start();
19 | };
20 |
21 | start = async () => {
22 | if (!this.state.isGameRunning) {
23 | this.game = new Game(this.getContext());
24 | await this.game.init();
25 | this.renderGame();
26 | }
27 | this.setState(state => ({ isGameRunning: !state.isGameRunning }));
28 | };
29 |
30 | renderGame = () => {
31 | requestAnimationFrame(() => {
32 | this.game.render();
33 |
34 | if (this.state.isGameRunning) {
35 | this.renderGame();
36 | }
37 | });
38 | };
39 |
40 | getContext = () => this.canvasRef.current.getContext("2d");
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Tilemaps examples (with React)
47 |
48 |
49 | No Scroll
50 |
51 |
52 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/classes/game.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Loader from "./loader";
4 | import TileMap from "./tileMap";
5 |
6 | export default class Game {
7 | constructor(context) {
8 | this.context = context;
9 | this.loader = new Loader();
10 | this.tileMap = new TileMap();
11 | }
12 |
13 | init = async () => {
14 | const tiles = await this.loader.loadImage("tiles", "./assets/tiles.png");
15 | this.tileAtlas = this.loader.getImage("tiles");
16 | this.images = {
17 | tiles,
18 | };
19 | };
20 |
21 | draw = () => {
22 | for (let columnIndex = 0; columnIndex < this.tileMap.columns; columnIndex++) {
23 | for (let rowIndex = 0; rowIndex < this.tileMap.rows; rowIndex++) {
24 | let tile = this.tileMap.getTile(columnIndex, rowIndex);
25 | if (tile !== 0) { // 0 => empty tile
26 | this.context.drawImage(
27 | this.tileAtlas, // image
28 | (tile - 1) * this.tileMap.tileSize, // source x
29 | 0, // source y
30 | this.tileMap.tileSize, // source width
31 | this.tileMap.tileSize, // source height
32 | columnIndex * this.tileMap.tileSize, // target x
33 | rowIndex * this.tileMap.tileSize, // target y
34 | this.tileMap.tileSize, // target width
35 | this.tileMap.tileSize // target height
36 | );
37 | }
38 | }
39 | }
40 | };
41 |
42 | render() {
43 | // draw map
44 | this.draw(0);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/classes/loader.js:
--------------------------------------------------------------------------------
1 | export default class Loader {
2 | constructor() {
3 | this.images = {};
4 | }
5 |
6 | loadImage = (key, src) => {
7 | const image = new Image();
8 | const promise = new Promise((resolve, reject) => {
9 | image.onload = () => {
10 | this.images[key] = image;
11 | resolve(image);
12 | };
13 |
14 | image.onerror = () => {
15 | reject("Could not load image: " + src);
16 | };
17 | });
18 |
19 | image.src = src;
20 | return promise;
21 | };
22 |
23 | getImage = key => {
24 | return key in this.images ? this.images[key] : null;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/classes/tileMap.js:
--------------------------------------------------------------------------------
1 | export default class TileMap {
2 | constructor() {
3 | this.columns = 8;
4 | this.rows = 8;
5 | this.tileSize = 64;
6 | this.tiles = [
7 | 1, 3, 3, 3, 1, 1, 3, 1,
8 | 1, 1, 1, 1, 1, 1, 1, 1,
9 | 1, 1, 1, 1, 1, 2, 1, 1,
10 | 1, 1, 1, 1, 1, 1, 1, 1,
11 | 1, 1, 1, 2, 1, 1, 1, 1,
12 | 1, 1, 1, 1, 2, 1, 1, 1,
13 | 1, 1, 1, 1, 2, 1, 1, 1,
14 | 1, 1, 1, 1, 2, 1, 1, 1
15 | ];
16 | }
17 |
18 | getTile(columnIndex, rowIndex) {
19 | return this.tiles[rowIndex * this.columns + columnIndex];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/square/non-scrolling-tilemaps/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/README.md:
--------------------------------------------------------------------------------
1 | # Scrolling tilemaps
2 |
3 | 
4 |
5 | ### Setup
6 |
7 | - Clone this repo
8 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
9 |
10 | - Go to project directory
11 | `cd react-tilemaps-examples/square/scrolling-tilemaps`
12 |
13 | - Install npm packages with yarn
14 | `yarn`
15 |
16 | ### Usage
17 |
18 | - Start the app on localhost:3000 !
19 | `yarn start`
20 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrolling-tilemaps",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.0"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/public/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/scrolling-tilemaps/public/assets/tiles.png
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/scrolling-tilemaps/public/favicon.ico
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Source Code Pro', monospace;
3 | }
4 |
5 | .header {
6 | height: 50px;
7 | padding: 10px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #31ffd5;
12 | font-size: 30px;
13 | font-weight: 600;
14 | }
15 |
16 | .subheader {
17 | height: 20px;
18 | padding: 10px;
19 | padding-top: 20px;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | }
24 |
25 | .subheader2 {
26 | height: 20px;
27 | padding-bottom: 30px;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
33 | .gameContainer {
34 | height: 100%;
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | background-color: #ffffff;
39 | }
40 |
41 | .flex {
42 | padding: 10;
43 | margin-top: 20px;
44 | justify-content: center;
45 | align-items: center;
46 | display: flex;
47 | }
48 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Game from './classes/game';
3 | import "./App.css";
4 |
5 | const CANVAS_WIDTH = 512;
6 | const CANVAS_HEIGHT = 512;
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | isGameRunning: false
13 | };
14 | this.canvasRef = React.createRef();
15 | }
16 |
17 | componentDidMount = () => {
18 | this.start();
19 | };
20 |
21 | start = async () => {
22 | if (!this.state.isGameRunning) {
23 | this.game = new Game(this.getContext());
24 | await this.game.init();
25 | this.renderGame();
26 | }
27 | this.setState(state => ({ isGameRunning: !state.isGameRunning }));
28 | };
29 |
30 | renderGame = () => {
31 | requestAnimationFrame((elapsed) => {
32 | this.game.render(elapsed);
33 |
34 | if (this.state.isGameRunning) {
35 | this.renderGame();
36 | }
37 | });
38 | };
39 |
40 | getContext = () => this.canvasRef.current.getContext("2d");
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Tilemaps examples (with React)
47 |
48 |
49 | Scrolling map
50 |
51 |
52 | Use arrow keys to move
53 |
54 |
55 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/classes/camera.js:
--------------------------------------------------------------------------------
1 | export default class Camera {
2 | constructor(map, width, height) {
3 | this.x = 0;
4 | this.y = 0;
5 | this.width = width;
6 | this.height = height;
7 | this.maxX = map.columns * map.tileSize - width;
8 | this.maxY = map.rows * map.tileSize - height;
9 | this.SPEED = 256; // pixels per second
10 | }
11 |
12 | move = (delta, dirX, dirY) => {
13 | // move camera
14 | this.x += dirX * this.SPEED * delta;
15 | this.y += dirY * this.SPEED * delta;
16 | // clamp values
17 | this.x = Math.max(0, Math.min(this.x, this.maxX));
18 | this.y = Math.max(0, Math.min(this.y, this.maxY));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/classes/game.js:
--------------------------------------------------------------------------------
1 | import Loader from "./loader";
2 | import TileMap from "./tileMap";
3 | import Camera from "./camera";
4 | import Keyboard from "./keyboard";
5 |
6 | export default class Game {
7 | constructor(context) {
8 | this.context = context;
9 | this.loader = new Loader();
10 | this.tileMap = new TileMap();
11 | this.camera = new Camera(this.tileMap, 512, 512);
12 | this.keyboard = new Keyboard();
13 | this._previousElapsed = 0;
14 | }
15 |
16 | init = async () => {
17 | this.keyboard.listenForEvents(
18 | [this.keyboard.LEFT, this.keyboard.RIGHT, this.keyboard.UP, this.keyboard.DOWN]);
19 | const tiles = await this.loader.loadImage("tiles", "./assets/tiles.png");
20 | this.tileAtlas = this.loader.getImage("tiles");
21 | this.images = {
22 | tiles,
23 | };
24 | };
25 |
26 | drawLayer = layerIndex => {
27 | var startColumn = Math.floor(this.camera.x / this.tileMap.tileSize);
28 | var endColumn = startColumn + (this.camera.width / this.tileMap.tileSize);
29 | var startRow = Math.floor(this.camera.y / this.tileMap.tileSize);
30 | var endRow = startRow + (this.camera.height / this.tileMap.tileSize);
31 | var offsetX = -this.camera.x + startColumn * this.tileMap.tileSize;
32 | var offsetY = -this.camera.y + startRow * this.tileMap.tileSize;
33 |
34 | for (let columnIndex = startColumn; columnIndex < endColumn; columnIndex++) {
35 | for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
36 | let tile = this.tileMap.getTile(layerIndex, columnIndex, rowIndex);
37 | const x = (columnIndex - startColumn) * this.tileMap.tileSize + offsetX;
38 | const y = (rowIndex - startRow) * this.tileMap.tileSize + offsetY;
39 | if (tile !== 0) { // 0 => empty tile
40 | this.context.drawImage(
41 | this.tileAtlas, // image
42 | (tile - 1) * this.tileMap.tileSize, // source x
43 | 0, // source y
44 | this.tileMap.tileSize, // source width
45 | this.tileMap.tileSize, // source height
46 | Math.round(x), // target x
47 | Math.round(y), // target y
48 | this.tileMap.tileSize, // target width
49 | this.tileMap.tileSize // target height
50 | );
51 | }
52 | }
53 | }
54 | };
55 |
56 | update = delta => {
57 | // handle camera movement with arrow keys
58 | let dirX = 0;
59 | let dirY = 0;
60 | if (this.keyboard.isDown(this.keyboard.LEFT)) { dirX = -1; }
61 | if (this.keyboard.isDown(this.keyboard.RIGHT)) { dirX = 1; }
62 | if (this.keyboard.isDown(this.keyboard.UP)) { dirY = -1; }
63 | if (this.keyboard.isDown(this.keyboard.DOWN)) { dirY = 1; }
64 |
65 | this.camera.move(delta, dirX, dirY);
66 | };
67 |
68 | getDelta = elapsed => {
69 | // compute delta time in seconds -- also cap it
70 | let delta = (elapsed - this._previousElapsed) / 1000.0;
71 | delta = Math.min(delta, 0.25); // maximum delta of 250 ms
72 | this._previousElapsed = elapsed;
73 | return delta;
74 | }
75 |
76 | render(elapsed) {
77 | this.context.clearRect(0, 0, 512, 512);
78 | this.update(this.getDelta(elapsed));
79 | // draw map background layer
80 | this.drawLayer(0);
81 | // draw map top layer
82 | this.drawLayer(1);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/classes/keyboard.js:
--------------------------------------------------------------------------------
1 | export default class Keyboard {
2 | constructor() {
3 | this.LEFT = 37;
4 | this.RIGHT = 39;
5 | this.UP = 38;
6 | this.DOWN = 40;
7 | this._keys = {};
8 | }
9 |
10 | listenForEvents = keys => {
11 | window.addEventListener('keydown', this._onKeyDown);
12 | window.addEventListener('keyup', this._onKeyUp);
13 |
14 | keys.forEach(key => {
15 | this._keys[key] = false;
16 | });
17 | }
18 |
19 | _onKeyDown = event => {
20 | const keyCode = event.keyCode;
21 | if (keyCode in this._keys) {
22 | event.preventDefault();
23 | this._keys[keyCode] = true;
24 | }
25 | };
26 |
27 | _onKeyUp = event => {
28 | const keyCode = event.keyCode;
29 | if (keyCode in this._keys) {
30 | event.preventDefault();
31 | this._keys[keyCode] = false;
32 | }
33 | };
34 |
35 | isDown = keyCode => {
36 | if (!keyCode in this._keys) {
37 | throw new Error(`Keycode ${keyCode} is not being listened to`);
38 | }
39 | return this._keys[keyCode];
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/classes/loader.js:
--------------------------------------------------------------------------------
1 | export default class Loader {
2 | constructor() {
3 | this.images = {};
4 | }
5 |
6 | loadImage = (key, src) => {
7 | const image = new Image();
8 | const promise = new Promise((resolve, reject) => {
9 | image.onload = () => {
10 | this.images[key] = image;
11 | resolve(image);
12 | };
13 |
14 | image.onerror = () => {
15 | reject("Could not load image: " + src);
16 | };
17 | });
18 |
19 | image.src = src;
20 | return promise;
21 | };
22 |
23 | getImage = key => {
24 | return key in this.images ? this.images[key] : null;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/classes/tileMap.js:
--------------------------------------------------------------------------------
1 | export default class TileMap {
2 | constructor() {
3 | this.columns = 12;
4 | this.rows = 12;
5 | this.tileSize = 64;
6 | this.layers = [[
7 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
8 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
9 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
10 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
11 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
12 | 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
13 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
14 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
15 | 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3,
16 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
17 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
18 | 3, 3, 3, 1, 1, 2, 3, 3, 3, 3, 3, 3
19 | ], [
20 | 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
21 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
22 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
23 | 4, 0, 0, 5, 0, 0, 0, 0, 0, 5, 0, 4,
24 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
25 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
26 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
27 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
28 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
29 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
30 | 4, 4, 4, 0, 5, 4, 4, 4, 4, 4, 4, 4,
31 | 4, 4, 4, 0, 0, 3, 3, 3, 3, 3, 3, 3
32 | ]];
33 | }
34 |
35 | getTile(layerIndex, columnIndex, rowIndex) {
36 | return this.layers[layerIndex][rowIndex * this.columns + columnIndex];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/square/scrolling-tilemaps/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/README.md:
--------------------------------------------------------------------------------
1 | # Tilemaps with layers
2 |
3 | 
4 |
5 | ### Setup
6 |
7 | - Clone this repo
8 | `git clone https://github.com/fatihturgut/react-tilemaps-examples.git`
9 |
10 | - Go to project directory
11 | `cd react-tilemaps-examples/square/tilemaps-with-layers`
12 |
13 | - Install npm packages with yarn
14 | `yarn`
15 |
16 | ### Usage
17 |
18 | - Start the app on localhost:3000 !
19 | `yarn start`
20 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example1-layers",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "3.0.0"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/public/assets/character.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/tilemaps-with-layers/public/assets/character.png
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/public/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/tilemaps-with-layers/public/assets/tiles.png
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fatihturgut/react-tilemaps-examples/e366a9e1689f92b6056d3cf26f0f62c0499ce9f8/square/tilemaps-with-layers/public/favicon.ico
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Source Code Pro', monospace;
3 | }
4 |
5 | .header {
6 | height: 50px;
7 | padding: 10px;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | background-color: #31ffd5;
12 | font-size: 30px;
13 | font-weight: 600;
14 | }
15 |
16 | .subheader {
17 | height: 20px;
18 | padding: 10px;
19 | margin-top: 20px;
20 | margin-bottom: 30px;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | }
25 |
26 | .gameContainer {
27 | height: 100%;
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | background-color: #ffffff;
32 | }
33 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Game from './classes/game';
3 | import "./App.css";
4 |
5 | const CANVAS_WIDTH = 512;
6 | const CANVAS_HEIGHT = 512;
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | isGameRunning: false
13 | };
14 | this.canvasRef = React.createRef();
15 | }
16 |
17 | componentDidMount = () => {
18 | this.start();
19 | };
20 |
21 | start = async () => {
22 | if (!this.state.isGameRunning) {
23 | this.game = new Game(this.getContext());
24 | await this.game.init();
25 | this.renderGame();
26 | }
27 | this.setState(state => ({ isGameRunning: !state.isGameRunning }));
28 | };
29 |
30 | renderGame = () => {
31 | requestAnimationFrame(() => {
32 | this.game.render();
33 |
34 | if (this.state.isGameRunning) {
35 | this.renderGame();
36 | }
37 | });
38 | };
39 |
40 | getContext = () => this.canvasRef.current.getContext("2d");
41 |
42 | render() {
43 | return (
44 |
45 |
46 | Tilemaps examples (with React)
47 |
48 |
49 | Layers
50 |
51 |
52 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/classes/game.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import Loader from "./loader";
4 | import TileMap from "./tileMap";
5 |
6 | export default class Game {
7 | constructor(context) {
8 | this.context = context;
9 | this.loader = new Loader();
10 | this.tileMap = new TileMap();
11 | }
12 |
13 | init = async () => {
14 | const tiles = await this.loader.loadImage("tiles", "./assets/tiles.png");
15 | const character = await this.loader.loadImage("character", "./assets/character.png");
16 | this.tileAtlas = this.loader.getImage("tiles");
17 | this.hero = { x: 128, y: 384, image: this.loader.getImage("character") };
18 | this.images = {
19 | tiles,
20 | character
21 | };
22 | };
23 |
24 | drawLayer = layerIndex => {
25 | for (let columnIndex = 0; columnIndex < this.tileMap.columns; columnIndex++) {
26 | for (let rowIndex = 0; rowIndex < this.tileMap.rows; rowIndex++) {
27 | let tile = this.tileMap.getTile(layerIndex, columnIndex, rowIndex);
28 | if (tile !== 0) { // 0 => empty tile
29 | this.context.drawImage(
30 | this.tileAtlas, // image
31 | (tile - 1) * this.tileMap.tileSize, // source x
32 | 0, // source y
33 | this.tileMap.tileSize, // source width
34 | this.tileMap.tileSize, // source height
35 | columnIndex * this.tileMap.tileSize, // target x
36 | rowIndex * this.tileMap.tileSize, // target y
37 | this.tileMap.tileSize, // target width
38 | this.tileMap.tileSize // target height
39 | );
40 | }
41 | }
42 | }
43 | };
44 |
45 | render() {
46 | // draw map background layer
47 | this.drawLayer(0);
48 | // draw game sprites
49 | this.context.drawImage(this.hero.image, this.hero.x, this.hero.y);
50 | // draw map top layer
51 | this.drawLayer(1);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/classes/loader.js:
--------------------------------------------------------------------------------
1 | export default class Loader {
2 | constructor() {
3 | this.images = {};
4 | }
5 |
6 | loadImage = (key, src) => {
7 | const image = new Image();
8 | const promise = new Promise((resolve, reject) => {
9 | image.onload = () => {
10 | this.images[key] = image;
11 | resolve(image);
12 | };
13 |
14 | image.onerror = () => {
15 | reject("Could not load image: " + src);
16 | };
17 | });
18 |
19 | image.src = src;
20 | return promise;
21 | };
22 |
23 | getImage = key => {
24 | return key in this.images ? this.images[key] : null;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/classes/tileMap.js:
--------------------------------------------------------------------------------
1 | export default class TileMap {
2 | constructor() {
3 | this.columns = 12;
4 | this.rows = 12;
5 | this.tileSize = 64;
6 | this.layers = [[
7 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
8 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
9 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
10 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
11 | 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
12 | 3, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3,
13 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
14 | 3, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3,
15 | 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 3,
16 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
17 | 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3,
18 | 3, 3, 3, 1, 1, 2, 3, 3, 3, 3, 3, 3
19 | ], [
20 | 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
21 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
22 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
23 | 4, 0, 0, 5, 0, 0, 0, 0, 0, 5, 0, 4,
24 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
25 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
26 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
27 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
28 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
29 | 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
30 | 4, 4, 4, 0, 5, 4, 4, 4, 4, 4, 4, 4,
31 | 4, 4, 4, 0, 0, 3, 3, 3, 3, 3, 3, 3
32 | ]];
33 | }
34 |
35 | getTile(layerIndex, columnIndex, rowIndex) {
36 | return this.layers[layerIndex][rowIndex * this.columns + columnIndex];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/square/tilemaps-with-layers/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------