タイピングゲームを作ろう
こんにちはkazutoです今回は、JavaScriptでタイピングゲームを作成していきます。
事前準備
まずは事前準備を行いましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="typing.css">
<script src="typing.js"defer></script>
</head>
<body>
<h1 class="heading">Typing games</h1>
<main>
<div class = "timers">
<p id = "text"></p>
<p id="timer">00:00.000</p>
<div class="buttons">
<button class="start" id="start">Start</button>
<button class="reset" id="reset">Reset</button>
<button class="up" id="up">+</button>
<button class="down" id="down">-</button>
</div>
</div>
<div class = "typingEria">
<p class=text>
<span id="typed"></span><span id="untyped"></span>
</p>
</div>
<div class="msk">
<div class= "modal">
<h1>Finished‼</h1>
<div class="successTypedBox">
<p class="successTyped">success</p>
<p class="successTypedNum"></p>
</div>
<div class="failureTypedBox">
<p class="failureTyped">failure</p>
<p class="failureTypedNum"> </p>
</div>
<div class="totalBox">
<p class="total">total </p>
<p class="totalNum"></p>
</div>
<div class="rankBox">
<p class="rank">rank </p>
<p class="rankNum"></p>
</div>
</div>
</div>
</div>
</main>
</body>
</html>
body{
font-family: cursive;
background-color: #222222;
}
body.msk{
height: 100%;
width: 100%;
}
.blog_name{
font-size: 120px;
color: white;
}
.app_name{
font-size: 80px;
color: white;
text-align: center;
}
.modal{
width: 600px;
height: 400px;
background-color: black;
opacity: 0;
transition: 5s;
margin: 0 90px;
margin-top: 70px;
border:solid 10px #111111;
}
.active{
opacity: 1;
}
.modal p{
color: white;
font-size: 40px;
text-align: center;
}
.successTypedBox{
display: flex;
justify-content: center;
padding: 0 30px;
}
.successTypedBox p{
margin: 0 20px;
margin-right:60px;
}
.failureTypedBox{
display: flex;
justify-content: center;
padding: 0 30px;
}
.failureTypedBox p{
margin: 0 20px;
margin-right:60px;
}
.totalBox{
display: flex;
justify-content: center;
padding: 0 30px;
}
.totalBox p{
margin: 0 35px;
margin-right:60px;
}
.rankBox{
display: flex;
justify-content: center;
padding: 0 30px;
}
.rankBox p{
margin: 0 35px;
margin-right:60px;
}
.heading{
text-align: center;
color: aqua;
}
main{
margin: auto;
width: 800px;
height: 600px;
}
.modal h1{
color: white;
text-align: center;
font-size: 50px;
margin: 0;
margin-top: 20px;
}
.timers{
border-radius: 40px;
margin: auto;
height: 80px;
}
.msk{
width: 100px;
height: 100px;
background-color: #222222;
}
#timer{
font-family:'DSEG';
width: 150px;
font-size: 20px;
margin: auto;
padding: 10px 0;
padding-left:30px;
background-color: black;
border:solid 10px #111111;
color: aqua;
border-radius: 30px;
}
.buttons{
width: 30%;
height: 60%;
margin: auto;
padding-left: 68px;
padding-top: 20px;
}
button{
background-color: black;
color: white;
font-size: 15px;
margin: 0 3px;
margin-top: 10px;
color: aqua;
}
.typingEria{
position: relative;
width: 600px;
height: 400px;
margin: auto;
background: black;
margin-top: 60px;
border:solid 10px #111111;
font-size: 80px;
text-align: center;
font-family: inherit;
}
.text{
margin: auto;
margin-top: 114px;
}
#typed{
color: aqua;
}
#untyped{
color: aliceblue ;
}
h1{
text-align: center;
}
.none{
display: none;
}
.block{
display: block;
}
#start_app{
font-size: 60px;
border-radius:30px ;
padding: 0 40px;
color: white;
}
#start_app:hover{
color: aqua;
}
.start_app_box{
width: 100%;
height: 100px;
display: flex;
justify-content: center;
padding-top: 30px;
}
img{
width: 100px;
height: 100px;
z-index: 20;
}
const timer = DomAcquisition("#timer")
const start = DomAcquisition("#start")
const reset = DomAcquisition("#reset")
const plus = DomAcquisition("#up")
const minus = DomAcquisition("#down")
let startTime ;
let remainingTime = 0;
let elapsedTime = 0;
let timeLeft;
let timeId;
let run =false
const buttons =[start,reset,plus,minus]
function DomAcquisition(element){
return document.querySelector(element)
}
function addClass(element){
element.classList.add("active")
}
function removeClass(element){
element.classList.remove("active")
}
function InsertText(el,content){
el.textContent = content
}
function updateTimer(t){
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms = d.getMilliseconds()
m=m.toString().padStart(2,"0")
s=s.toString().padStart(2,"0")
ms=ms.toString().padStart(3,"0")
timer.textContent = `${m}:${s}.${ms}`
}
function timeCountDown(){
timerId=setInterval(function(){
elapsedTime =new Date() - startTime
timeLeft = remainingTime - elapsedTime
updateTimer(timeLeft)
if (timeLeft<=0){
clearTimeout(timerId)
remainingTime = 0
timeLeft= 0
run = false
InsertText(start,"Start")
gameEnd()
return updateTimer(timeLeft)
}
}, 10);
}
function timeSet(sign){
switch (sign) {
case "+":
remainingTime+=1000*60
break;
case "-":
remainingTime-=1000*60
break;
}
updateTimer(remainingTime)
}
function gameStart (){
setText()
InsertText(untyped,unTypedBox)
document.body.addEventListener("keyup",keyupEvent)
}
function gameEnd(){
document.body.removeEventListener("keyup",keyupEvent)
modal.classList.add("active")
document.querySelector(".typingEria").classList.add("none")
}
function Update(){
InsertText(typed,typedBox)
InsertText(untyped,unTypedBox)
}
function setText(){
return unTypedBox =typingTexts[Math.floor(Math.random()*typingTexts.length)]
}
function mouseoverAndMouseleaveEvent (){
addClass(this)
this.addEventListener("mouseleave",()=>removeClass(this))
}
function startClickEvent(){
if(remainingTime<=0){return}
if(run===false){
gameStart()
startTime =new Date()
timeCountDown()
run=true
InsertText(start,"Stop")
}else{
document.body.removeEventListener("keyup",keyupEvent)
InsertText(start,"Start")
remainingTime = timeLeft
clearTimeout(timerId)
updateTimer(remainingTime)
run=false
}
}
function resetClickEvent(){
clearInterval(timerId)
remainingTime = 0
timeLeft = 0
run =false
InsertText(start,"Start")
return updateTimer(remainingTime)
}
function plusClickEvent(){
if(run ===false){
if ( remainingTime >=3540000) {
remainingTime = 3600000
timer.textContent = '60:00.000'
return
}
timeSet("+")
}
}
function minusClickEvent(){
if(run==false && remainingTime > 0){ timeSet("-") }
}
function addEvent(eventTarget,eventName,callBack){
eventTarget.addEventListener(eventName,callBack)
}
function buttonsEvents(eventName){
buttons.forEach(eventTarget=>{
if (eventName==="mouseover") {
addEvent(eventTarget,eventName,mouseoverAndMouseleaveEvent)
}else{
switch (eventTarget) {
case start:
addEvent(eventTarget,eventName,startClickEvent)
break;
case reset:
addEvent(eventTarget,eventName,resetClickEvent)
break;
case plus:
addEvent(eventTarget, eventName,plusClickEvent)
break;
case minus:
addEvent(eventTarget,eventName,minusClickEvent)
break;
}
}
})
}
const eventNames=["mouseover","click"]
eventNames.forEach(eventName=>{buttonsEvents(eventName)})
※今回はタイマー機能について解説しません。タイマー機能を作ってみたい方は、下記のURLから参照してください。
タイピングゲームを作成していきますが、まずは、どの様にタイマー機能と連結するかを考えていきましょう。
タイピングゲームの構成を考えよう
今回、作成するタイピングゲームは、ユーザーが「キーを打つ」という動作を行い、画面に出ている文字列と一致した場合「色が変わる」という動作を取ります。
この様な仕様からkeyupイベントを使う事が分かりますね?
タイピングゲームを実装するにあたり気をつけるべきポイントは、タイピング機能とタイマー機能の関係性
です。この関係性が崩れてしまうと、ゲーム性が失われ、もはやタイピングゲームでは、無くなってしまいます。なのでkeyupイベントをシチュエーション
によって柔軟に
- 登録
- 削除
を行う必要があります。
上記のフローチャートを確認してみましょう。
まず初めにゲームがスタート、つまり、タイマーが起動したタイミングでkeyupイベントを登録していきます。
その後、タイマーを止めた場合は、keyupイベントを削除し、タイマーを再起動した場合にイベントを再登録していきます。
この様にシチュエーションによって、keyupイベントを登録&削除を繰り返す事でタイピングゲームの保守生
を保っています。
では実際に機能を実装していきましょう。
タイピングゲーム
- ノードを取得
- イベント登録&関数化
- イベント発火後の処理を実装
- タイマー機能と連結させよう
ノードを取得
まずは、イベントに必要なノードを取得しておきましょう。ついでに変数についても定義しておきましょう
function DomAcquisition(element){
return document.querySelector(element)
}
const typed = DomAcquisition("#typed")
const untyped = DomAcquisition("#untyped")
let typedBox=``
let unTypedBox=``
let successTyped=0;
let failureTyped=0;
const typed = DomAcquisition("#typed")
const untyped = DomAcquisition("#untyped")
こちらのノードは、「タイプが成功したら色が変わる」という動作を再現したいために定義した関数です。
let typedBox=``
let unTypedBox=``
変数unTypedBoxにはタイプがされていない文字列が格納され、変数typedBoxにはタイプが成功した文字列が格納されていきます。こちらの変数は、タイピング機能の根幹を担う部分
なので忘れずに定義しておいてください。
let successTyped=0;
let failureTyped=0;
こちらは、
- タイピング成功数
- タイピング失敗数
を格納しておくための変数です。
最後にノードを正しく取得できているかデバックしましょう。
forLogs=(element)=>{
element.forEach(el=>{
console.log(el);
})
}
forLogs([typed,untyped])
ノードを取得できているのが確認できたら次のステップに進みましょう。
イベント登録&関数化
先ほどのトピックでノードを取得できたので、こちらのトピックでは、イベントを登録していきましょう。今回は、body要素にkeyupイベントを登録していきます。またタイマー機能に埋め込む形でイベントを登録
をしたいので、関数化して機動力を上げていきます。
const keyupEvent=(el)=>{
}
document.body.addEventListener("keyup",keyupEvent)
上記のソースコードでイベントが登録できましたので、実際にイベントが登録できたかデバックしていきましょう。
const keyupEvent=(el)=>{
console.log("test")
}
document.body.addEventListener("keyup",keyupEvent)
testという文字列が出力されている事が確認できたら次のステップに進みましょう。
イベント発火後の処理を実装
イベントが登録できた所でイベント発火後の処理を実装していきましょう。
function setText(){
return unTypedBox =typingTexts[Math.floor(Math.random()*typingTexts.length)]
}
function InsertText(el,content){
el.textContent = content
}
function Update(){
InsertText(typed,typedBox)
InsertText(untyped,unTypedBox)
}
const keyupEvent=(el)=>{
if(el.key!==unTypedBox.substring(0,1)){return failureTyped++;}
typedBox += unTypedBox.substring(0,1)
unTypedBox = unTypedBox.substring(1)
successTyped++
Update()
if(unTypedBox==``){
typedBox=``
unTypedBox =setText()
Update()
}
}
if(el.key!==unTypedBox.substring(0,1)){return failureTyped++;}
こちらのソースコードは、タイプに失敗した場合と条件分岐をしています。タイプしたkeyが対象の文字と等しくない場合、returnメソッドを用いて処理を中断をして、変数failureTypedに1を足します。
変数failureTypedに値を更新する意図は、ゲーム終了後にスコア結果として、失敗したタイプ数を表示
するためです。
typedBox += unTypedBox.substring(0,1)
タイプに成功した場合は、「タイプに成功した文字の色を変える」という動作を実現したいために、変数typedBoxには変数unTypedBoxに対して、substringメソッド用いて、先頭の文字を抽出をして格納しています。
unTypedBox = unTypedBox.substring(1)
その後、文字列の差分を埋めるため、変数unTypedBoxに対して再代入する形で、substringメソッドを用いて、先頭の要素を除いた全ての要素を抽出し
、格納しています。この作業を行わないと差分が発生してエラーが起こります。
successTyped++
Update()
変数successTypedに対して、タイプが成功する事に1を足して、タイプスコア数を更新します。
関数Updateを用いて新しいHTML要素を上書きする形で挿入をし、結果的に「タイプに成功した文字の色を変える」という動作を再現をしています。
if(unTypedBox==``){
typedBox=``
unTypedBox = setText()
Update()
}
変数unTypedBoxの値が空の文字列の場合は、画面に表示されている文字列が全てタイプに成功されている事になるので、ゲームの性質上、変数unTypedBoxに新しい文字列を生成しなければなりません。したがって、一度
- 変数TypedBox
- 変数unTypedBox
を初期化しなければなりません。
function setText(){
return unTypedBox =typingTexts[Math.floor(Math.random()*typingTexts.length)]
}
typedBox=``
unTypedBox = setText()
Update()
変数typedBoxは、空の文字列にし、変数unTypedBoxは、関数setTextを用いて、ランダムな文字列を生成し、格納しています。
その後、変更内容を関数Updateを用いて更新をしています。
以上でタイピング機能の実装は、終わりです。続いてタイピング機能をタイマー機能に組み込んいきましょう。
タイマー機能と連結させよう
タイマー機能にタイピング機能を組み込む前にもう一度、タイピングゲームのフローチャートを確認しておきましょう。
- ゲームスタート
→keyupイベントを登録 - ゲーム中断
→keyupイベント削除 - ゲーム再開
→keyupイベントを再登録 - ゲーム終了
→→keyupイベント削除
と上記の様になります。後は、パズルみたいに当てはめていくだけです。
function gameStart (){
setText()
InsertText(untyped,unTypedBox)
//追加
document.body.addEventListener("keyup",keyupEvent)
}
function gameEnd(){
//追加
document.body.removeEventListener("keyup",keyupEvent)
InsertText(successTypedFiled,`${successTyped}`)
InsertText(failureTypedFiled,`${failureTyped}`)
InsertText(total,`${successTyped+failureTyped}`)
InsertText(rankFiled ,rank())
modal.classList.add("active")
document.querySelector(".typingEria").classList.add("none")
}
function startClickEvent(){
if(remainingTime<=0){return}
if(run===false){
gameStart()
startTime =new Date()
timeCountDown()
run=true
InsertText(start,"Stop")
}else{
//追加
document.body.removeEventListener("keyup",keyupEvent)
typedBox = ``
unTypedBox =setText()
Update()
InsertText(start,"Start")
remainingTime = timeLeft
clearTimeout(timerId)
updateTimer(remainingTime)
run=false
}
}
function resetClickEvent(){
//追加
document.body.removeEventListener("keyup",keyupEvent)
clearInterval(timerId)
remainingTime = 0
timeLeft = 0
run =false
InsertText(start,"Start")
return updateTimer(remainingTime)
}
上記の様にイベント登録&削除を行ってください。
function gameStart (){
setText()
InsertText(untyped,unTypedBox)
//追加
document.body.addEventListener("keyup",keyupEvent)
}
上記の様に、イベント登録&削除を行ってください。なお細かい解説については省かせてもらいます。
以上で「タイピングゲーム」のトピックが終了になります。実装の最後にスコアをモーダルウインドウに表示していきましょう。
モーダルウインドウにスコアを表示しよう
- ノードの取得or変数定義
- スコアを表示しよう
ノードの取得or変数定義
まずは、モーダルウインドウに必要な
- ノード
- 変数
を取得・定義していきましょう。
const total = DomAcquisition(".totalNum")
const successTypedFiled =DomAcquisition(".successTypedNum")
const failureTypedFiled = DomAcquisition(".failureTypedNum")
const modal = DomAcquisition(".modal")
const rankFiled = DomAcquisition(".rankNum")
let successTyped=0;
let failureTyped=0;
let rankd=``
const total = DomAcquisition(".totalNum")
const successTypedFiled =DomAcquisition(".successTypedNum")
const failureTypedFiled = DomAcquisition(".failureTypedNum")
const modal = DomAcquisition(".modal")
const rankFiled = DomAcquisition(".rankNum")
こちらの定数達は、モーダルウインドウにスコアを表示するためのノードです。ゲーム終了後、関数InsertTextを用いて、各種項目を挿入していきます。
let successTyped=0;
let failureTyped=0;
let rankd=``
変数の詳細は上から順に
- タイプ成功数
- タイプ失敗数
- ランク
となります。
トータルタイプ数に関してはタイプ成功数とタイプ失敗数を足せば良いだけなので特に変数は定義をしないです。
トピックの最後に、念のためデバックしておきましょう。
forLogs=(element)=>{
element.forEach(el=>{
console.log(el);
})
}
forLogs([total,successTypedFiled,failureTypedFiled,modal,rankFiled])
画像の様にノードが確認できれば次のステップに進みましょう。
スコアを表示しよう
最後にスコアを表示していきましょう。
function rank (score) {
if(score>=1000){
return "SS"
}else if(score>=800){
return "S"
}else if(score >=600){
return "A"
}else if(score>=400){
return "B"
}else if(score>=200){
return"C"
}else{
return"D"
}
}
function gameEnd(){
const totalScore = successTyped+failureTyped
document.body.removeEventListener("keyup",keyupEvent)
//追加
InsertText(successTypedFiled,`${successTyped}`)
InsertText(failureTypedFiled,`${failureTyped}`)
InsertText(total,`${totalScore}`)
InsertText(rankFiled ,rank(totalScore))
modal.classList.add("active")
document.querySelector(".typingEria").classList.add("none")
}
function gameEnd(){
const totalScore = successTyped+failureTyped
document.body.removeEventListener("keyup",keyupEvent)
//追加
InsertText(successTypedFiled,`${successTyped}`)
InsertText(failureTypedFiled,`${failureTyped}`)
InsertText(total,`${totalScore}`)
InsertText(rankFiled ,rank(totalScore))
modal.classList.add("active")
document.querySelector(".typingEria").classList.add("none")
}
モーダルウインドウが表示されるのは、ゲームが終了した時
なのでゲームを終了する際に作動する関数gameEndに組み込めば、意図通りの挙動になります。
function rank (score) {
if(score>=1000){
return "SS"
}else if(score>=800){
return "S"
}else if(score >=600){
return "A"
}else if(score>=400){
return "B"
}else if(score>=200){
return"C"
}else{
return"D"
}
}
ランク機能を作成してみましたが、おまけ機能みたいな物なので、実装するかどうかは、お任せします。
以上で、タイピングゲームの実装は終了です。完成したソースコードは、下記に貼り付けておきます。
const timer = DomAcquisition("#timer")
const start = DomAcquisition("#start")
const reset = DomAcquisition("#reset")
const plus = DomAcquisition("#up")
const minus = DomAcquisition("#down")
let startTime ;
let remainingTime = 0;
let elapsedTime = 0;
let timeLeft;
let timeId;
let run =false
const typed = DomAcquisition("#typed")
const untyped = DomAcquisition("#untyped")
const total = DomAcquisition(".totalNum")
const successTypedFiled =DomAcquisition(".successTypedNum")
const failureTypedFiled = DomAcquisition(".failureTypedNum")
const modal = DomAcquisition(".modal")
const rankFiled = DomAcquisition(".rankNum")
let successTyped=0;
let failureTyped=0;
let rankd=``
const typingTexts=
["HTML","CSS","JavaScript","Go","Ruby","RubyonRails",
"Hellow","fa","game","targets","timer","api","apple",
"soccer","strawberry","persimmon","kiwifruit","cherry",
"pear","pineapple","banana","grape","React"]
let typedBox=``
let unTypedBox=``
const buttons =[start,reset,plus,minus]
function DomAcquisition(element){
return document.querySelector(element)
}
function addClass(element){
element.classList.add("active")
}
function removeClass(element){
element.classList.remove("active")
}
function InsertText(el,content){
el.textContent = content
}
function updateTimer(t){
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms = d.getMilliseconds()
m=m.toString().padStart(2,"0")
s=s.toString().padStart(2,"0")
ms=ms.toString().padStart(3,"0")
timer.textContent = `${m}:${s}.${ms}`
}
function timeCountDown(){
timerId=setInterval(function(){
elapsedTime =new Date() - startTime
timeLeft = remainingTime - elapsedTime
updateTimer(timeLeft)
if (timeLeft<=0){
clearTimeout(timerId)
remainingTime = 0
timeLeft= 0
run = false
InsertText(start,"Start")
gameEnd()
return updateTimer(timeLeft)
}
}, 10);
}
function timeSet(sign){
switch (sign) {
case "+":
remainingTime+=1000*60
break;
case "-":
remainingTime-=1000*60
break;
}
updateTimer(remainingTime)
}
function gameStart (){
if (typedBox!==``) {return}
setText()
InsertText(untyped,unTypedBox)
}
function gameEnd(){
const totalScore = successTyped+failureTyped
document.body.removeEventListener("keyup",keyupEvent)
InsertText(successTypedFiled,`${successTyped}`)
InsertText(failureTypedFiled,`${failureTyped}`)
InsertText(total,`${totalScore}`)
InsertText(rankFiled ,rank(totalScore))
modal.classList.add("active")
document.querySelector(".typingEria").classList.add("none")
}
function Update(){
InsertText(typed,typedBox)
InsertText(untyped,unTypedBox)
}
function setText(){
return unTypedBox =typingTexts[Math.floor(Math.random()*typingTexts.length)]
}
function rank (score) {
if(score>=1000){
return "SS"
}else if(score>=800){
return "S"
}else if(score >=600){
return "A"
}else if(score>=400){
return "B"
}else if(score>=200){
return"C"
}else{
return"D"
}
}
function nextText(){
typedBox=``
unTypedBox =setText()
Update()
}
const keyupEvent=(el)=>{
if(el.key!==unTypedBox.substring(0,1)){return failureTyped++;}
typedBox += unTypedBox.substring(0,1)
unTypedBox = unTypedBox.substring(1)
successTyped++
Update()
if(unTypedBox==``){ nextText()}
}
function mouseoverAndMouseleaveEvent (){
addClass(this)
this.addEventListener("mouseleave",()=>removeClass(this))
}
function startClickEvent(){
if(remainingTime<=0){return}
if(run===false){
document.body.addEventListener("keyup",keyupEvent)
gameStart()
startTime =new Date()
timeCountDown()
run=true
InsertText(start,"Stop")
}else{
document.body.removeEventListener("keyup",keyupEvent)
InsertText(start,"Start")
remainingTime = timeLeft
clearTimeout(timerId)
updateTimer(remainingTime)
run=false
}
}
function resetClickEvent(){
typedBox = ``
unTypedBox = ``
Update()
document.body.removeEventListener("keyup",keyupEvent)
clearInterval(timerId)
remainingTime = 0
timeLeft = 0
run =false
InsertText(start,"Start")
return updateTimer(remainingTime)
}
function plusClickEvent(){
if(run ===false){
if ( remainingTime >=3540000) {
remainingTime = 3600000
timer.textContent = '60:00.000'
return
}
timeSet("+")
}
}
function minusClickEvent(){
if(run==false && remainingTime > 0){ timeSet("-") }
}
function addEvent(eventTarget,eventName,callBack){
eventTarget.addEventListener(eventName,callBack)
}
function buttonsEvents(eventName){
buttons.forEach(eventTarget=>{
if (eventName==="mouseover") {
addEvent(eventTarget,eventName,mouseoverAndMouseleaveEvent)
}else{
switch (eventTarget) {
case start:
addEvent(eventTarget,eventName,startClickEvent)
break;
case reset:
addEvent(eventTarget,eventName,resetClickEvent)
break;
case plus:
addEvent(eventTarget, eventName,plusClickEvent)
break;
case minus:
addEvent(eventTarget,eventName,minusClickEvent)
break;
}
}
})
}
const eventNames=["mouseover","click"]
eventNames.forEach(eventName=>{buttonsEvents(eventName)})
まとめ:タイピングゲームを作ろう
今回は、JavaScriptでタイピングゲームを作ってみました。結構、本格的なタイピングゲームだったと思います。今回、素のJavaScriptでタイピングゲームを作った理由は、他のライブラリやフレームワークに応用できる点です。
実際に、Reactでもタイピングゲームを作成してみました。興味がある方はこちらのURLからご参照してください。
以上、kazutoでした。
関連記事