ゲーム本体はこちらにあります。
React.jsとは何か? みたいな話は適当にぐぐってください。
さて、 Canvas版ではCanvasにテトリスの画面を描画しましたが、 仮想DOMで画面を構築するReact.jsを使いながら画面描画にCanvasを使っていては React.jsのメリットも何もないわけで、React.js版では画面自体はtable要素で組んでいます。
コンポーネント指向のReact.jsのメリットを生かして、 ゲームのステージ本体と、Next欄は同じコンポーネント(Boardクラス)を使用している、 のですが、メリットというほどのものなんだろうかこれ……
ネット上にいくらでも情報は転がっていますが、私自身のメモも兼ねて。
この手の環境構築情報は、公式サイトを見るのが定石です。 日本語で読みたい気持ちはわかりますが、情報が古くてはまることが多いものです。 私の場合、使い方を調べるためにチュートリアルのページを読んだので、 そこにある手順で導入しました。
私のPCはWindowsですが、以下の手順で構築できました。
コマンドラインで「npx create-react-app (アプリケーション名)」とタイプします。 こんな感じ。
C:\maebashi\temp>mkdir reacttetris C:\maebashi\temp>cd reacttetris C:\maebashi\temp\reacttetris>npx create-react-app reacttetris npx: installed 1 in 2.765s Path must be a string. Received undefined npx: installed 91 in 12.11s C:\Users\kmaebashi\AppData\Roaming\npm-cache\_npx\10072\node_modules\create-react-app\index.js Creating a new React app in C:\maebashi\temp\reacttetris\reacttetris. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts... > core-js@2.6.9 postinstall C:\maebashi\temp\reacttetris\reacttetris\node_modules\babel-runtime\node_modules\core-js > node scripts/postinstall || echo "ignore" > core-js-pure@3.1.4 postinstall C:\maebashi\temp\reacttetris\reacttetris\node_modules\core-js-pure > node scripts/postinstall || echo "ignore" + react@16.8.6 + react-dom@16.8.6 + react-scripts@3.0.1 added 1405 packages in 105.713s Success! Created reacttetris at C:\maebashi\temp\reacttetris\reacttetris Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd reacttetris npm start Happy hacking! C:\maebashi\temp\reacttetris>
インターネットにつながっていれば、これで必要なパッケージも、 (このディレクトリに)入ります。
C:\maebashi\temp\reacttetris\reacttetris>npm start > reacttetris@0.1.0 start C:\maebashi\temp\reacttetris\reacttetris > react-scripts start Starting the development server... Compiled successfully! You can now view reacttetris in the browser. Local: http://localhost:3000/ On Your Network: http://192.168.56.1:3000/ Note that the development build is not optimized. To create a production build, use npm run build.
1: { 2: "name": "reacttetris", 3: "version": "0.1.0", 4: "private": true, 5: "homepage": "./", ←この行を追加 6: "dependencies": { 7: "react": "^16.8.6", 8: "react-dom": "^16.8.6", (後略)その上で、「npm run build」を実行。
C:\maebashi\temp\reacttetris\reacttetris>npm run build > reacttetris@0.1.0 build C:\maebashi\temp\reacttetris\reacttetris > react-scripts build Creating an optimized production build... Compiled successfully. File sizes after gzip: 36.83 KB build\static\js\2.98dcc9dd.chunk.js 1.95 KB build\static\js\main.41dbc1c1.chunk.js 763 B build\static\js\runtime~main.d653cc00.js 515 B build\static\css\main.eb45528a.chunk.css The project was built assuming it is hosted at ./. You can control this with the homepage field in your package.json. The build folder is ready to be deployed. Find out more about deployment here: https://bit.ly/CRA-deploy C:\maebashi\temp\reacttetris\reacttetris>これでbuildフォルダにできたファイル一式をどこかのWebサーバに置いて、 index.htmlにアクセスすれば、アプリケーションを使用できます。
Githubにも上げてありますが、
https://github.com/kmaebashi/reacttetris
別段動かしたいわけでもないけれど、ソースをざっと眺めたい、という人は以下を参照してください。
./index.js
1: import React from 'react'; 2: import ReactDOM from 'react-dom'; 3: import './index.css'; 4: 5: class Board extends React.Component { 6: render() { 7: return ( 8: <table className="board-table"> 9: <tbody> 10: {this.props.table.map((row, index) => { 11: return ( 12: <tr key={index}> 13: {row.map((td, index) => <td key={index} className={"block-type-" + td}></td>)} 14: </tr> 15: ); 16: })} 17: </tbody> 18: </table> 19: ); 20: } 21: } 22: 23: class TetrisInformationPanel extends React.Component { 24: render() { 25: return ( 26: <div> 27: <p>LINES:{this.props.lines}</p> 28: <p>{this.props.message}</p> 29: </div> 30: ); 31: } 32: } 33: 34: class Tetris extends React.Component { 35: constructor(props) { 36: super(props); 37: this.stageWidth = 10; 38: this.stageHeight = 20; 39: this.nextAreaSize = 6; 40: this.blocks = this.createBlocks(); 41: this.deletedLines = 0; 42: this.message = ""; 43: // boardTable, nextTableは、[y][x]の形式でアクセスする。 44: // HTMLのtableの生成順序と合わせるため。 45: this.boardTable = this.create2DArray(this.stageHeight, this.stageWidth); 46: this.nextTable = this.create2DArray(this.nextAreaSize, this.nextAreaSize); 47: 48: this.state = { 49: boardTable: this.clone2DArray(this.boardTable), 50: nextTable: this.clone2DArray(this.nextTable), 51: deletedLines: this.deletedLines, 52: message: this.message, 53: }; 54: 55: window.onkeydown = (e) => { 56: if (this.currentBlock == null) { 57: return; 58: } 59: if (e.keyCode === 37) { 60: this.moveLeft(); 61: } else if (e.keyCode === 38) { 62: this.rotate(); 63: } else if (e.keyCode === 39) { 64: this.moveRight(); 65: } else if (e.keyCode === 40) { 66: this.fall(); 67: } 68: } 69: } 70: 71: componentDidMount() { 72: this.startGame(); 73: } 74: 75: create2DArray(rows, cols) { 76: let array = new Array(rows); 77: for (let i = 0; i < rows; i++) { 78: array[i] = new Array(cols).fill(null); 79: } 80: return array; 81: } 82: 83: clone2DArray(src) { 84: let dest = new Array(src.length); 85: for (let i = 0; i < src.length; i++) { 86: dest[i] = src[i].slice(); 87: } 88: return dest; 89: } 90: 91: createBlocks() { 92: let blocks = [ 93: { 94: shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]], 95: [[0, -1], [0, 0], [0, 1], [0, 2]], 96: [[-1, 0], [0, 0], [1, 0], [2, 0]], 97: [[0, -1], [0, 0], [0, 1], [0, 2]]] 98: }, 99: { 100: shape: [[[0, 0], [1, 0], [0, 1], [1, 1]], 101: [[0, 0], [1, 0], [0, 1], [1, 1]], 102: [[0, 0], [1, 0], [0, 1], [1, 1]], 103: [[0, 0], [1, 0], [0, 1], [1, 1]]] 104: }, 105: { 106: shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]], 107: [[-1, -1], [-1, 0], [0, 0], [0, 1]], 108: [[0, 0], [1, 0], [-1, 1], [0, 1]], 109: [[-1, -1], [-1, 0], [0, 0], [0, 1]]] 110: }, 111: { 112: shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]], 113: [[0, -1], [-1, 0], [0, 0], [-1, 1]], 114: [[-1, 0], [0, 0], [0, 1], [1, 1]], 115: [[0, -1], [-1, 0], [0, 0], [-1, 1]]] 116: }, 117: { 118: shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]], 119: [[0, -1], [1, -1], [0, 0], [0, 1]], 120: [[-1, 0], [0, 0], [1, 0], [1, 1]], 121: [[0, -1], [0, 0], [-1, 1], [0, 1]]] 122: }, 123: { 124: shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]], 125: [[0, -1], [0, 0], [0, 1], [1, 1]], 126: [[-1, 0], [0, 0], [1, 0], [-1, 1]], 127: [[-1, -1], [0, -1], [0, 0], [0, 1]]] 128: }, 129: { 130: shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]], 131: [[0, -1], [0, 0], [1, 0], [0, 1]], 132: [[-1, 0], [0, 0], [1, 0], [0, 1]], 133: [[0, -1], [-1, 0], [0, 0], [0, 1]]] 134: } 135: ]; 136: return blocks; 137: } 138: 139: drawBlock(x, y, type, angle, table) { 140: let block = this.blocks[type]; 141: for (let i = 0; i < block.shape[angle].length; i++) { 142: this.drawCell(table, 143: x + block.shape[angle][i][0], 144: y + block.shape[angle][i][1], 145: type); 146: } 147: } 148: 149: drawCell(table, x, y, type) { 150: if (y < 0) { 151: return; 152: } 153: table[y][x] = type; 154: } 155: 156: startGame() { 157: // virtualStageは、xxTableと異なり、[x][y]の形式でアクセスする。 158: this.virtualStage = this.create2DArray(this.stageWidth, this.stageHeight); 159: this.currentBlock = null; 160: this.nextBlock = this.getRandomBlock(); 161: this.mainLoop(); 162: } 163: 164: mainLoop() { 165: if (this.currentBlock == null) { 166: if (!this.createNewBlock()) { 167: return; 168: } 169: } else { 170: this.fallBlock(); 171: } 172: this.drawStage(); 173: if (this.currentBlock != null) { 174: this.drawBlock(this.blockX, this.blockY, 175: this.currentBlock, this.blockAngle, this.boardTable); 176: } 177: this.refresh(); 178: setTimeout(this.mainLoop.bind(this), 500); 179: } 180: 181: refresh() { 182: this.setState({ 183: boardTable: this.clone2DArray(this.boardTable), 184: nextTable: this.clone2DArray(this.nextTable), 185: deletedLines: this.deletedLines, 186: message: this.message, 187: }); 188: } 189: 190: createNewBlock() { 191: this.currentBlock = this.nextBlock; 192: this.nextBlock = this.getRandomBlock(); 193: this.blockX = Math.floor(this.stageWidth / 2 - 2); 194: this.blockY = 0; 195: this.blockAngle = 0; 196: this.drawNextBlock(); 197: this.refresh(); 198: if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) { 199: this.message = "GAME OVER"; 200: this.refresh(); 201: return false; 202: } 203: return true; 204: } 205: 206: drawNextBlock() { 207: this.clear(this.nextTable); 208: this.drawBlock(2, 1, this.nextBlock, 0, this.nextTable); 209: } 210: 211: getRandomBlock() { 212: return Math.floor(Math.random() * 7); 213: } 214: 215: fallBlock() { 216: if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) { 217: this.blockY++; 218: } else { 219: this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle); 220: this.currentBlock = null; 221: } 222: } 223: 224: checkBlockMove(x, y, type, angle) { 225: for (let i = 0; i < this.blocks[type].shape[angle].length; i++) { 226: let cellX = x + this.blocks[type].shape[angle][i][0]; 227: let cellY = y + this.blocks[type].shape[angle][i][1]; 228: if (cellX < 0 || cellX > this.stageWidth - 1) { 229: return false; 230: } 231: if (cellY > this.stageHeight - 1) { 232: return false; 233: } 234: if (this.virtualStage[cellX][cellY] != null) { 235: return false; 236: } 237: } 238: return true; 239: } 240: 241: fixBlock(x, y, type, angle) { 242: for (let i = 0; i < this.blocks[type].shape[angle].length; i++) { 243: let cellX = x + this.blocks[type].shape[angle][i][0]; 244: let cellY = y + this.blocks[type].shape[angle][i][1]; 245: this.virtualStage[cellX][cellY] = type; 246: } 247: for (let y = this.stageHeight - 1; y >= 0; ) { 248: let filled = true; 249: for (let x = 0; x < this.stageWidth; x++) { 250: if (this.virtualStage[x][y] == null) { 251: filled = false; 252: break; 253: } 254: } 255: if (filled) { 256: for (let y2 = y; y2 > 0; y2--) { 257: for (let x = 0; x < this.stageWidth; x++) { 258: this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1]; 259: } 260: } 261: this.deletedLines++; 262: } else { 263: y--; 264: } 265: } 266: } 267: 268: drawStage() { 269: this.clear(this.boardTable); 270: 271: for (let x = 0; x < this.virtualStage.length; x++) { 272: for (let y = 0; y < this.virtualStage[x].length; y++) { 273: if (this.virtualStage[x][y] != null) { 274: this.drawCell(this.boardTable, 275: x, y, this.virtualStage[x][y]); 276: } 277: } 278: } 279: } 280: 281: moveLeft() { 282: if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) { 283: this.blockX--; 284: this.refreshStage(); 285: } 286: } 287: 288: moveRight() { 289: if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) { 290: this.blockX++; 291: this.refreshStage(); 292: } 293: } 294: 295: rotate() { 296: let newAngle; 297: if (this.blockAngle < 3) { 298: newAngle = this.blockAngle + 1; 299: } else { 300: newAngle = 0; 301: } 302: if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) { 303: this.blockAngle = newAngle; 304: this.refreshStage(); 305: } 306: } 307: 308: fall() { 309: while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) { 310: this.blockY++; 311: this.refreshStage(); 312: } 313: } 314: 315: refreshStage() { 316: this.clear(this.boardTable); 317: this.drawStage(); 318: if (this.currentBlock != null) { 319: this.drawBlock(this.blockX, this.blockY, 320: this.currentBlock, this.blockAngle, this.boardTable); 321: } 322: this.refresh(); 323: } 324: 325: clear(table) { 326: for (let y = 0; y < table.length; y++) { 327: for (let x = 0; x < table[y].length; x++) { 328: table[y][x] = null; 329: } 330: } 331: } 332: 333: render() { 334: return ( 335: <div className="wrapper-container"> 336: <span className="tetris-container"> 337: <Board 338: table = {this.state.boardTable} 339: /> 340: <span className="tetris-panel-container"> 341: <p>Next:</p> 342: <Board 343: table = {this.state.nextTable} 344: /> 345: <TetrisInformationPanel 346: lines = {this.state.deletedLines} 347: message = {this.state.message} 348: /> 349: <div className="tetris-panel-container-padding"></div> 350: <table className="tetris-button-panel"> 351: <tbody> 352: <tr> 353: <td></td> 354: <td id="tetris-rotate-button" className="tetris-button" onMouseDown = {this.rotate.bind(this)}>↻</td> 355: <td></td> 356: </tr> 357: <tr> 358: <td id="tetris-move-left-button"className="tetris-button" onMouseDown = {this.moveLeft.bind(this)}>←</td> 359: <td id="tetris-fall-button" className="tetris-button" onMouseDown = {this.fall.bind(this)}>↓</td> 360: <td id="tetris-move-right-button" className="tetris-button" onMouseDown = {this.moveRight.bind(this)}>→</td> 361: </tr> 362: </tbody> 363: </table> 364: </span> 365: </span> 366: </div> 367: ); 368: } 369: } 370: 371: // ======================================== 372: 373: ReactDOM.render( 374: <Tetris />, 375: document.getElementById('root') 376: );
CSSです。ここで、td要素に「block-type-(ブロック種別)」というクラスに色を与えることで、 各種のブロックを表示しています。
index.css
1: html { 2: touch-action: manipulation; 3: } 4: 5: .wrapper-container { 6: display: inline-block; 7: } 8: 9: .tetris-container { 10: display: flex; 11: flex-direction: row; 12: margin: 10px; 13: background-color: #333333; 14: } 15: 16: .board-table { 17: border-collapse: separate; 18: border-spacing: 0; 19: } 20: .board-table td { 21: width: 25px; 22: height: 25px; 23: margin: 0px; 24: padding: 0px; 25: border: solid 1px black; 26: background-color: black; 27: } 28: 29: td.block-type-0 { 30: background-color: #00ffff; 31: border-top: 1px #ffffff solid; 32: border-left: 1px #ffffff solid; 33: border-right: 1px #008080 solid; 34: border-bottom: 1px #008080 solid; 35: } 36: 37: td.block-type-1 { 38: background-color: #ffff00; 39: border-top: 1px #ffffff solid; 40: border-left: 1px #ffffff solid; 41: border-right: 1px #808000 solid; 42: border-bottom: 1px #808000 solid; 43: } 44: 45: td.block-type-2 { 46: background-color: #00ff00; 47: border-top: 1px #ffffff solid; 48: border-left: 1px #ffffff solid; 49: border-right: 1px #008000 solid; 50: border-bottom: 1px #008000 solid; 51: } 52: 53: td.block-type-3 { 54: background-color: #ff0000; 55: border-top: 1px #ffffff solid; 56: border-left: 1px #ffffff solid; 57: border-right: 1px #800000 solid; 58: border-bottom: 1px #800000 solid; 59: } 60: 61: td.block-type-4 { 62: background-color: #0000ff; 63: border-top: 1px #ffffff solid; 64: border-left: 1px #ffffff solid; 65: border-right: 1px #000080 solid; 66: border-bottom: 1px #000080 solid; 67: } 68: 69: td.block-type-5 { 70: background-color: #ffa500; 71: border-top: 1px #ffffff solid; 72: border-left: 1px #ffffff solid; 73: border-right: 1px #ff5200 solid; 74: border-bottom: 1px #ff5200 solid; 75: } 76: 77: td.block-type-6 { 78: background-color: #ff00ff; 79: border-top: 1px #ffffff solid; 80: border-left: 1px #ffffff solid; 81: border-right: 1px #800080 solid; 82: border-bottom: 1px #800080 solid; 83: } 84: 85: .tetris-panel-container { 86: display: flex; 87: padding-left: 10px; 88: padding-right: 10px; 89: flex-direction: column; 90: color: white; 91: background-color: #333333; 92: } 93: 94: .tetris-panel-container-padding { 95: flex-grow: 1; 96: } 97: 98: .tetris-panel-container p { 99: margin: 0; 100: padding: 0; 101: } 102: 103: .tetris-button-panel { 104: border-style: none; 105: width: 100%; 106: } 107: 108: .tetris-button { 109: padding-top: 10px; 110: padding-bottom: 10px; 111: text-align: center; 112: background: #444444; 113: }
11: return ( 12: <tr key={index}> 13: {row.map((td, index) = {td ? <td key={index} className={"block-type-" + td}></td> 14: : <td key={index}></td>)} 15: </tr> 16: );なんか挙動がはっきりしない…… Firefoxのインスペクタで見たHTMLは、想定通りなんですが。