├── 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 | ![react-tilemaps](https://media.giphy.com/media/S6ksBiFYWSoPJLW48w/giphy.gif) 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 | ![offcanvas-drawing](https://i.ibb.co/Zcm1Fmj/offcanvas-drawing.png) 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 | ![collisions-in-tilemaps](https://i.ibb.co/HVMRFbP/collisions-in-tilemaps.png) 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 | ![non-scrolling-tilemaps](https://i.ibb.co/jhRTBqQ/non-scrolling-tilemaps.png) 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 | ![scrolling-tilemaps](https://i.ibb.co/DKCDD49/scrolling.png) 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 | ![tilemaps-with-layers](https://i.ibb.co/55cGMhP/tilemaps-with-layers.png) 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 | --------------------------------------------------------------------------------