仮面ライダーが好きすぎてタイピングゲーム作ったんだ
突然ですが、私は仮面ライダーが好きです。
その中でもクウガは特別な存在です。
あの臨場感、人間味溢れるストーリー、CG技術じゃ表せないリアル感、主人公の戦い方。
平成ライダーの先駆けとなった変身!を知らしめた作品でもあります。
クウガにも様々なフォームがあります。楽しく覚えたいですよね。
なのでタイピングしながらやれば楽しく、無意識に覚えるのではないかと考えました。
ということで作成した次第です。
こんなものです
タイマーを右上に設置しています。
あとはスタートするとランダムにフォームデータが表示され、タイピングします。
終わった後はタイプ数とミスタイプ数が表示される、という簡易的なものです。
使用環境
TypeScriptのみです。コンパイルにWebpackを使用しています。
UIはtailwindcssのcdnを読み込んで使用しています。
以下、GitHubソースコードです。
コード
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>仮面ライダークウガ-タイピング</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="h-screen">
<div class="flex flex-col justify-between h-full">
<header class="py-2 px-4 bg-blue-400">
<div class="text-white flex justify-between items-end">
<h1 class="text-2xl">Rider_Kuuga Typing</h1>
<p>残り時間 <span id="timer"></span>秒</p>
</div>
</header>
<main class="py-20">
<div class="mx-auto container">
<div class="text-center intro">
<figure class="mb-8">
<img class="mx-auto" src="images/thumbsup.jpg" alt="thumbsup">
</figure>
<button id="start-btn" class="bg-blue-400 text-white rounded font-bold px-4 py-2">ゲーム開始</button>
</div>
<div class="start hidden">
<figure>
<img id="riderImg" class="mx-auto" src="" alt="">
</figure>
<div class="mt-16 text-center">
<span id="riderNameJp"></span>
<p id="riderNameEn" class="mb-4 text-4xl"></p>
</div>
</div>
<div class="finished hidden">
<div class="text-center">
<p>
タイピング数: <span id="score"></span>
</p>
<p>
ミスタイプ: <span id="miss"></span>
</p>
<button id="reset-btn" class="bg-blue-400 text-white rounded font-bold px-4 py-2 mt-4">もう一回</button>
</div>
</div>
</div>
</main>
<footer class="py-2 px-4 text-center bg-blue-400">
<small class="text-white">©2020 iwata</small>
</footer>
</div>
<script type="module" src="dist/main.js"></script>
</body>
</html>
constants.ts
export const riders = [
{
'name': {
'en': 'mightyform',
'ja': 'マイティフォーム'
},
'src': 'images/mighty.jpg'
},
{
'name': {
'en': 'dragonform',
'ja': 'ドラゴンフォーム'
},
'src': 'images/dragon.jpg'
},
{
'name': {
'en': 'pegasusform',
'ja': 'ペガサスフォーム'
},
'src': 'images/pegasus.jpg'
},
{
'name': {
'en': 'titanform',
'ja': 'タイタンフォーム'
},
'src': 'images/titan.jpg'
},
{
'name': {
'en': 'amazingmighty',
'ja': 'アメイジングマイティ'
},
'src': 'images/amazingmighty.jpg'
},
{
'name': {
'en': 'ultimateform',
'ja': 'アルティメットフォーム'
},
'src': 'images/ultimate.jpg'
},
{
'name': {
'en': 'gourum',
'ja': 'ゴウラム'
},
'src': 'images/gourum.jpg'
}
]
main.ts
import { riders } from './constants'
type RiderProps = typeof riders
type RiderNameProps = {
ja: string
en: string
}
const intro = document.querySelector('.intro')
const start = document.querySelector('.start')
const finished = document.querySelector('.finished')
const score = document.getElementById('score')
const miss = document.getElementById('miss')
const timer = document.getElementById('timer')
const startBtn = document.getElementById('start-btn')
const resetBtn = document.getElementById('reset-btn')
const nameEn = document.getElementById('riderNameEn')
const nameJp = document.getElementById('riderNameJp')
const imageArea = document.getElementById('riderImg') as HTMLImageElement
class GameState {
private readonly riders: RiderProps
selectedRiderName: RiderNameProps
selectedRiderImg: string
score: number = 0
miss: number = 0
limitTimer: number = 10
protected constructor(riders) {
this.riders = riders
}
get getRiders() {
return this.riders
}
protected printData() {
nameEn.innerText = this.selectedRiderName.en
nameJp.innerText = this.selectedRiderName.ja
imageArea.src = this.selectedRiderImg
}
protected printTimer() {
timer.innerText = String(this.limitTimer)
}
protected printScore() {
score.innerText = String(this.score)
miss.innerText = String(this.miss)
}
}
class GameAction extends GameState {
private static instance: GameAction
static getInstance() {
if (!GameAction.instance) {
GameAction.instance = new GameAction(riders)
}
return GameAction.instance
}
static switchPhase(phase) {
switch (phase) {
case 'start':
intro.classList.add('hidden')
start.classList.remove('hidden')
break
case 'finished':
start.classList.add('hidden')
finished.classList.remove('hidden')
break
case 'restart':
finished.classList.add('hidden')
intro.classList.remove('hidden')
}
}
init() {
const copyRider = JSON.parse(JSON.stringify(this.getRiders))
const rnd = Math.floor(Math.random() * copyRider.length)
const selectRider = copyRider[rnd]
this.selectedRiderName = selectRider.name
this.selectedRiderImg = selectRider.src
this.printData()
this.printTimer()
}
start() {
const countDown = setInterval(() => {
this.limitTimer--
this.printTimer()
if (!this.limitTimer) {
this.printScore()
GameAction.switchPhase('finished')
clearInterval(countDown)
return
}
}, 1000)
}
typing(key: string) {
const splitRiderName = this.selectedRiderName.en.split('')
if (key === splitRiderName[0]) {
this.score++
this.selectedRiderName.en = splitRiderName.slice(1).join('')
nameEn.innerText = this.selectedRiderName.en
if (!this.selectedRiderName.en) {
this.init()
}
return
}
this.miss++
}
reset() {
this.score = 0
this.miss = 0
this.limitTimer = 60
this.init()
}
}
const gameAction = GameAction.getInstance()
gameAction.init()
startBtn.addEventListener('click', () => {
gameAction.start()
GameAction.switchPhase('start')
})
document.addEventListener('keypress', function(event) {
gameAction.typing(event.key)
})
resetBtn.addEventListener('click', () => {
gameAction.reset()
GameAction.switchPhase('restart')
})
クラスベースに作成しています。
GameActionでスタート、タイピング、フィニッシュのアクション処理を集約し、GameState値を変更します。GameStateはstateを保持し、変化した値をdom上に描写するのみです。
GameActionをインスタンス化するとき、単一のインスタンスを使用するようシングルトンパターンを適応させています。staticメソッドのgetInstance
の部分です。
感想
最初はJSでやっていたのですが、「private protected修飾子なくね?というか型がないのが気持ち悪いな。」となり、途中からTSを導入しました。
好きなもののゲームを作成するとなったら、出来はどうであれ楽しいものです。