#はじめに
初めまして、@kashiwagi_wataruと申します。
プログラミングを初めて間もない頃って成果物を誰かに使ってもらうことが少なくてモチベーションの意地が結構難しいと思うんですよね。
なんか初心者の人でも誰かに使ってもらえるようなアプリを開発できる記事をかけないかなぁと考えてみました!
そこで思いついたのが**コピペでできる。テトリスを作って遊んでもらお!**と言う開発から公開まで一連の流れを体験できる記事です。
追記:OUTPUTの鬼となるため、初めてqiitaの記事を書かせていただくことになりました。
乱文、乱コードは寒い日の夜中に子供にプレゼントを届けるサンタのような目でみていただければ幸いです
#この記事の対象とする人
- プログラミングを初めてまもない人
- サーバーサイドが苦手な人
- 自分が作ったものを誰かに使ってみてもらいたい人
- 今この記事を見ている人
#やりたいことメモ
大人気ゲームテトリスを作る
スマホでも操作可能。
ホーム画面に追加をするとオフラインでも使えようにする
消した行をカウントする。
デプロイとか難しいことはしたくない。AWSで間違ってお金がかかったりするから(うっかり課金経験者ですw)
(AWSでお金がうっかり課金されちゃいガチな人はこちらへ笑 https://qiita.com/Yuji-Ishibashi/items/bb1c0042fd16a9350c5a )
#目次
大きく分けて3つに分けることができます。
- PWAの設定をする
- テトリスを作る
- githubで公開する
一応なぞってコピペしていくと完成するようになっていますが、
興味あるところまで飛ばしてくださっても全然大丈夫です!
#PWAとは?
みなさん、PWAってご存知ですか。
Googleが言い出した、今注目されつつある仕組みで、
これを使えばwebサイトのUIUXをよりよくできるだろうと言われているものになります。
WAとは、「Progressive Web Apps」の略称で、モバイル向けWebサイトをスマートフォン向けアプリのように使えるようにする仕組みです。
PWAはそれ自体が何か特殊な一つの技術、というわけではありません。レスポンシブデザイン、HTTPS化など、Googleが定める要素を備えたWebサイトであり、オフラインやプッシュ通知に対応するためのブラウザAPI(Service Workerなど)を利用しているWebサイトをPWAと呼びます。
PWAを実装することでプッシュ通知やホーム画面へのアイコン追加など、アプリの特徴的な機能をWebサイトに持たせる事ができます。これにより、UX向上やユーザーエンゲージメントの改善にもつながるとして注目されています。
つまり、webアプリケーションをあたかもスマホアプリのように使えるようになるとのことです!
「ホーム画面に追加」をするとキャッシュさえ残っていればオフラインでもそのwebページが利用可能になるとのこと。。
##PWAの導入
PWAの導入はすごく簡単です。
必要なファイルは下記の3つです。
- index.html
- service_worker.js
- manifest.json
圧倒的少なさ!
これだけでゲームが作れるなんて
では一つずつ紹介していきますね
##index.html
このファイルは一番最初に表示されるページとなります。
スマホでもたくさん使って欲しいのでviewportの記述を忘れずに、
とりあえずは下記のような感じでいいでしょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="manifest.json">
<title>Tetris_app</title>
</head>
<body>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service_worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
}
</script>
</body>
</html>
##service_worker.js
PWAにとってこのファイルはとても大事です。
push通知とかアプリチックなことを実行するときに重要なservice_workerの登録をしてくれます。
// キャッシュファイルの指定
var CACHE_NAME = 'TetrisApp-caches';
var urlsToCache = [
'/kashiwagi-wataru.github.io/',
];
// service workerの記述
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service_worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
}
// インストール処理
self.addEventListener('install', function(event) {
event.waitUntil(
caches
.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
// キャッシュロードの処理
self.addEventListener('fetch', function(event) {
event.respondWith(
caches
.match(event.request)
.then(function(response) {
return response ? response : fetch(event.request);
})
);
});
##manifest.json
これは、スマホでアプリみたいに利用できるようにするためのファイルです。
下記のような感じでいいでしょう
ポイントは"display": "standalone"です。
standaloneにすることでスマホでアプリのように使用することができます。
もっと拘りたい方は、
https://developer.mozilla.org/ja/docs/Web/Manifest
をみて改造してみてください
アイコンとかを指定できたりもします。
{
"short_name": "Tetris",
"name": "Tetris_App",
"display": "standalone",
"start_url": "index.html"
}
これで最低限必要なファイルは完成です!(簡単!)
#テトリスを作る
今からテトリスを作っていきます。
テトリスといえば知らない人はいないあのゲームのことですよね。
ゲームをつくるといっても使うのは、HTML
CSS
Javascript
の3つしか使いません。
先ほど作った3つのファイル+cssファイル,jsファイルを作りましょう。
下記のファイルは全て同じ階層に格納されます。
- index.html(このファイルに上書きしていきます)
- service_worker.js(これは触らない)
- manifest.json(これも触らない)
--下記2つを追加--
- style.css
- app.js
こんなのを作れればいいなぁと思ってます。
テトリスのコードはすごく長くなるのでザーッとみてコピペでいいですw
追々細かい説明を書いていきます笑(2020年になってから笑)
テトリスのコードは下記の通りです。
##index.html
先ほど作ったindex.htmlに追記(上書き)していきましょう
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="manifest" href="manifest.json">
<title>PWA Sample</title>
</head>
<body>
<div class="title-wrap">
<h1>テトリスで遊ぼう!</h1>
</div>
<div class="wrapper-container">
<div class="tetris-container">
<div class="tetris-main">
<canvas id="stage" width="250px" height="500px" style="background-color:black;">
</canvas>
</div>
<div class="tetris-option">
<span class="tetris-panel-container">
<p>Next</p>
<canvas id="next" width="150px" height="150px" style="background-color:black;">
</canvas>
<p>LINES:<span id="lines">0</span></p>
<p><span id="message"></span></p>
<div class="tetris-panel-container-padding">
<table class="tetris-button-panel">
<tr>
<td></td>
<td id="tetris-rotate-button" class="tetris-button">↻</td>
<td></td>
</tr>
<tr>
<td id="tetris-move-left-button"class="tetris-button">←</td>
<td id="tetris-fall-button"class="tetris-button">↓</td>
<td id="tetris-move-right-button"class="tetris-button">→</td>
</tr>
</table>
</div>
</span>
</div>
</div>
</div>
<script src="app.js"></script>
<script>
var tetris = new Tetris();
tetris.startGame();
</script>
</body>
</html>
##style.css
html {
touch-action: manipulation;
}
h1 {
margin: 0;
position: relative;
font-size: 40px;
text-align: center;
margin: 20px 0;
display: inline-block;
}
p {
margin: 0;
line-height: 2;
}
.title1-wrap {
padding-bottom: 20px;
text-align: center;
}
.wrapper-container {
display: inline-block;
}
.tetris-container {
height:530px;
display: flex;
flex-direction: row;
margin: 10px;
background-color: #333333;
}
.tetris-panel-contaizner {
display: flex;
padding-left: 10px;
padding-right: 10px;
flex-direction: column;
color: white;
background-color: #333333;
}
.tetris-panel-container-padding {
flex-grow: 1;
}
.tetris-panel-container p {
margin-left:20px;
padding: 10px 0;
font-family: sans-serif;
font-size: 20px;
color: #ffffff;
}
.tetris-button-panel {
border-style: none;
width: 100%;
padding-top:100px;
}
.tetris-button {
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
background: #444444;
box-shadow: inset 0 2px 0 rgba(255,255,255,0.2), inset 0 -2px 0 rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, .15);
border-radius: 4px;
}
.tetris-button:active {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.30);
}
##app.js
class Tetris {
constructor() {
this.stageWidth = 10;
this.stageHeight = 20;
this.stageCanvas = document.getElementById("stage");
this.nextCanvas = document.getElementById("next");
let cellWidth = this.stageCanvas.width / this.stageWidth;
let cellHeight = this.stageCanvas.height / this.stageHeight;
this.cellSize = cellWidth < cellHeight ? cellWidth : cellHeight;
this.stageLeftPadding = (this.stageCanvas.width - this.cellSize * this.stageWidth) / 2;
this.stageTopPadding = (this.stageCanvas.height - this.cellSize * this.stageHeight) ;
this.blocks = this.createBlocks();
this.deletedLines = 0;
window.onkeydown = (e) => {
if (e.keyCode === 37) {
this.moveLeft();
} else if (e.keyCode === 38) {
this.rotate();
} else if (e.keyCode === 39) {
this.moveRight();
} else if (e.keyCode === 40) {
this.fall();
}
}
document.getElementById("tetris-move-left-button").onmousedown = (e) => {
this.moveLeft();
}
document.getElementById("tetris-rotate-button").onmousedown = (e) => {
this.rotate();
}
document.getElementById("tetris-move-right-button").onmousedown = (e) => {
this.moveRight();
}
document.getElementById("tetris-fall-button").onmousedown = (e) => {
this.fall();
}
}
createBlocks() {
let blocks = [
{
shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]],
[[0, -1], [0, 0], [0, 1], [0, 2]],
[[-1, 0], [0, 0], [1, 0], [2, 0]],
[[0, -1], [0, 0], [0, 1], [0, 2]]],
color: "rgb(0, 255, 255)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(0, 128, 128)"
},
{
shape: [[[0, 0], [1, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [0, 1], [1, 1]]],
color: "rgb(255, 255, 0)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(128, 128, 0)"
},
{
shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]],
[[-1, -1], [-1, 0], [0, 0], [0, 1]],
[[0, 0], [1, 0], [-1, 1], [0, 1]],
[[-1, -1], [-1, 0], [0, 0], [0, 1]]],
color: "rgb(0, 255, 0)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(0, 128, 0)"
},
{
shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]],
[[0, -1], [-1, 0], [0, 0], [-1, 1]],
[[-1, 0], [0, 0], [0, 1], [1, 1]],
[[0, -1], [-1, 0], [0, 0], [-1, 1]]],
color: "rgb(255, 0, 0)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(128, 0, 0)"
},
{
shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]],
[[0, -1], [1, -1], [0, 0], [0, 1]],
[[-1, 0], [0, 0], [1, 0], [1, 1]],
[[0, -1], [0, 0], [-1, 1], [0, 1]]],
color: "rgb(0, 0, 255)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(0, 0, 128)"
},
{
shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]],
[[0, -1], [0, 0], [0, 1], [1, 1]],
[[-1, 0], [0, 0], [1, 0], [-1, 1]],
[[-1, -1], [0, -1], [0, 0], [0, 1]]],
color: "rgb(255, 165, 0)",
highlight: "rgb(255, 255, 255)",
shadow: "rgb(128, 82, 0)"
},
{
shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]],
[[0, -1], [0, 0], [1, 0], [0, 1]],
[[-1, 0], [0, 0], [1, 0], [0, 1]],
[[0, -1], [-1, 0], [0, 0], [0, 1]]],
color: "rgb(255, 0, 255)",
highlight: "rgb(255, 255, 255)",111111111111
shadow: "rgb(128, 0, 128)"
}
];
return blocks;
}
drawBlock(x, y, type, angle, canvas) {
let context = canvas.getContext("2d");
let block = this.blocks[type];
for (let i = 0; i < block.shape[angle].length; i++) {
this.drawCell(context,
x + (block.shape[angle][i][0] * this.cellSize),
y + (block.shape[angle][i][1] * this.cellSize),
this.cellSize,
type);
}
}
drawCell(context, cellX, cellY, cellSize, type) {
let block = this.blocks[type];
let adjustedX = cellX + 0.5;
let adjustedY = cellY + 0.5;
let adjustedSize = cellSize - 1;
context.fillStyle = block.color;
context.fillRect(adjustedX, adjustedY, adjustedSize, adjustedSize);
context.strokeStyle = block.highlight;
context.beginPath();
context.moveTo(adjustedX, adjustedY + adjustedSize);
context.lineTo(adjustedX, adjustedY);
context.lineTo(adjustedX + adjustedSize, adjustedY);
context.stroke();
context.strokeStyle = block.shadow;
context.beginPath();
context.moveTo(adjustedX, adjustedY + adjustedSize);
context.lineTo(adjustedX + adjustedSize, adjustedY + adjustedSize);
context.lineTo(adjustedX + adjustedSize, adjustedY);
context.stroke();
}
startGame() {
let virtualStage = new Array(this.stageWidth);
for (let i = 0; i < this.stageWidth; i++) {
virtualStage[i] = new Array(this.stageHeight).fill(null);
}
this.virtualStage = virtualStage;
this.currentBlock = null;
this.nextBlock = this.getRandomBlock();
this.mainLoop();
}
mainLoop() {
if (this.currentBlock == null) {
if (!this.createNewBlock()) {
return;
}
} else {
this.fallBlock();
}
this.drawStage();
if (this.currentBlock != null) {
this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
this.stageTopPadding + this.blockY * this.cellSize,
this.currentBlock, this.blockAngle, this.stageCanvas);
}
setTimeout(this.mainLoop.bind(this), 500);
}
createNewBlock() {
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.blockX = Math.floor(this.stageWidth / 2 - 2);
this.blockY = 0;
this.blockAngle = 0;
this.drawNextBlock();
if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) {
let messageElem = document.getElementById("message");
messageElem.innerText = "GAME OVER";
return false;
}
return true;
}
drawNextBlock() {
this.clear(this.nextCanvas);
this.drawBlock(this.cellSize * 2, this.cellSize, this.nextBlock,
0, this.nextCanvas);
}
getRandomBlock() {
return Math.floor(Math.random() * 7);
}
fallBlock() {
if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
this.blockY++;
} else {
this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle);
this.currentBlock = null;
}
}
checkBlockMove(x, y, type, angle) {
for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
let cellX = x + this.blocks[type].shape[angle][i][0];
let cellY = y + this.blocks[type].shape[angle][i][1];
if (cellX < 0 || cellX > this.stageWidth - 1) {
return false;
}
if (cellY > this.stageHeight - 1) {
return false;
}
if (this.virtualStage[cellX][cellY] != null) {
return false;
}
}
return true;
}
fixBlock(x, y, type, angle) {
for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
let cellX = x + this.blocks[type].shape[angle][i][0];
let cellY = y + this.blocks[type].shape[angle][i][1];
if (cellY >= 0) {
this.virtualStage[cellX][cellY] = type;
}
}
for (let y = this.stageHeight - 1; y >= 0; ) {
let filled = true;
for (let x = 0; x < this.stageWidth; x++) {
if (this.virtualStage[x][y] == null) {
filled = false;
break;
}
}
if (filled) {
for (let y2 = y; y2 > 0; y2--) {
for (let x = 0; x < this.stageWidth; x++) {
this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1];
}
}
for (let x = 0; x < this.stageWidth; x++) {
this.virtualStage[x][0] = null;
}
let linesElem = document.getElementById("lines");
this.deletedLines++;
linesElem.innerText = "" + this.deletedLines;
} else {
y--;
}
}
}
drawStage() {
this.clear(this.stageCanvas);
let context = this.stageCanvas.getContext("2d");
for (let x = 0; x < this.virtualStage.length; x++) {
for (let y = 0; y < this.virtualStage[x].length; y++) {
if (this.virtualStage[x][y] != null) {
this.drawCell(context,
this.stageLeftPadding + (x * this.cellSize),
this.stageTopPadding + (y * this.cellSize),
this.cellSize,
this.virtualStage[x][y]);
}
}
}
}
moveLeft() {
if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) {
this.blockX--;
this.refreshStage();
}
}
moveRight() {
if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) {
this.blockX++;
this.refreshStage();
}
}
rotate() {
let newAngle;
if (this.blockAngle < 3) {
newAngle = this.blockAngle + 1;
} else {
newAngle = 0;
}
if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) {
this.blockAngle = newAngle;
this.refreshStage();
}
}
fall() {
while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
this.blockY++;
this.refreshStage();
}
}
refreshStage() {
this.clear(this.stageCanvas);
this.drawStage();
this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
this.stageTopPadding + this.blockY * this.cellSize,
this.currentBlock, this.blockAngle, this.stageCanvas);
}
clear(canvas) {
let context = canvas.getContext("2d");
context.fillStyle = "rgb(0, 0, 0)";
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
#githubで公開する!!
では作成したファイルをサーバーに公開して終了となります。
サーバーはhttpsが利用できるAWSやherokuなどで頑張って公開しようかと思っていたところ、
githubだけで簡単にwebページを公開する方法があるそうです。
しかもgithubの公式さんが言うには、かなり簡単とのこと。
これはやるしかないですね
github公式が割とわかりやすいのでおいておきますね。
https://pages.github.com/
github.ioと言うものらしいです。
##github.ioのメリットデメリット
###メリット
- 簡単にwebページを公開できる
- 無料
- 更新したい場合はリポジトリにpushするだけ
###デメリット
- ソースコードが公開される
- サーバー依存のjsとかは使えない
今回は...
- ただ自分で作ったwebページを公開するだけ(特にセキリュティを気にするものはない)
- jsはサーバーに依存しない
と言うことで最適な方法と言えるのではないのでしょうか。
##方法
やり方は簡単で、
リポジトリ名を
username.github.io
にするだけでいいらしいです。すごい。
ではやってみましょう。(本当にほぼ公式のやり方をなぞっていくだけですw)
① まずはリポジトリを作ります。
命名規則はusername.github.io
だそうです。
僕の場合は下記のようになります。
② 次に下記のコマンドを実行します。デスクトップとかでね
クローンして
git clone https://github.com/username/username.github.io
移動してサンプルテキストを入れて(index.htmlを作って)
cd username.github.io
echo "Hello World" > index.html
git add --all
git commit -m "Initial commit"
git push -u origin master
そして https://username.github.io
にアクセスすればHelloWorldと表示されるはず!
なぜか404と怒られてしまいました。。。
原因はよくわかりませんw
ですが作ったリポジトリのsettings
にある
ここにリンクが載っていました。
リンクの構造がgithub公式の構造と少し違いますが、とりあえず踏んでみましょう。
表示されました!
(反映されるまで、少し時間が(30秒くらい)かかるかもしれないので焦らずに待ちましょう)
これで作ったゲームを公開する準備はできました。
このリポジトリにテトリスのファイルをぶち込んで完成となります。
#動作確認
最終的なファイルは
- index.html
- style.css
- app.js
- manifest.json
- service_worker.js
の5つになると思います。
githubのリポジトリに全てのファイルが揃ったらhttps://username.github.io
にアクセスしてみましょう。
PC、スマホ両方でアクセスすると良いと思います。
こんな感じでChromeとは別のタブで開けていたらOKです!
オフラインでの動作を確認したいので一度タブを閉じで機内モードにしてから開いてみてください
機内モードでもテトリスで遊べちゃうと思います!
恐るべしPWA。。。
こんな感じで、テトリスをスマホアプリのように扱えるようになりました。
リンクを友達に教えると友達のスマホでも遊べるようになるのでぜひ友達に送りつけてみてください。
#参照記事