【SvelteでRPGゲーム開発】コントローラーをコンポーネント化してスプライトを操作してみる
※ 当ページには【広告/PR】を含む場合があります。
2023/02/12
2023/02/14

前回までで、Javascriptゲームで欠かせない滑らかなアニメーション効果をつけるための
おそらく
requestAnimationFrame
逆に言うと、
今回はスプライトを
前回で難しさの“山場”は超えているので、さほど気構えることなく楽しくSvelteアプリを楽しんで作り込んでいきましょう。
この記事で目的としてかかげるポイントは以下の通りです。
1. 親子関係にあるSvelteコンポーネント間のイベントの渡し方
2. ゲーム画面上のコンポーネントを右クリック禁止にする
3. ゲーム画面上の文字テキストの選択を禁止にする
4. iframeのリソースキャッシュを無効化するメタタグの書き方
なお、最後まで実装していただくと以下のように動きます。 ※まだ表示要素のフレキシブル対応などを考慮していなので、スマホなどからでは表示サイズが適切に調整されません...ご了承ください。
説明
RPG風マップでネコ(?)を動かすだけの実験プログラムです。
画面下にあるコントローラーからの操作が有効になります。
現状は十字キーで上下左右に動くのみです。
Svelteでコントローラー風コンポーネントを準備する
今回も
Svelteプロジェクトの構成(のnodejs等の設定ファイルは除く)は前回から以下のようにしてみます。
※新規追加するファイル/フォルダには
☆
○
.
├── index.html
└── src
├── App.svelte
├── app.scss
├── main.ts
├── components
│ ├── player.svelte
│ ├── title.svelte
│ └── controller.svelte☆
└── lib
├── models.ts
└── GameStage.svelte○
#(その他のファイルは省略)
ということで
controller.svelte
いきなり完成品を丸々とお見せしますが、ファイルを追加したら、まず以下のように実装してみてください。
<script lang="ts">
export let cursorHandler: (dir: number, state: number) => void;
export let btnAHandler: (state: number) => void;
export let btnBHandler: (state: number) => void;
export let btnXHandler: (state: number) => void;
export let btnYHandler: (state: number) => void;
export let btnStartHandler: (state: number) => void;
export let btnSelectHandler: (state: number) => void;
</script>
<div class="spf-controller" on:contextmenu|preventDefault>
<div class="controller-left">
<div class="cross-layout">
<button class="position-top btn cross-key-btn"
on:mousedown={() => cursorHandler(1,0)}
on:mouseup={() => cursorHandler(1,1)}><span class="top-mark">▲</span></button>
<button class="position-left btn cross-key-btn"
on:mousedown={() => cursorHandler(2,0)}
on:mouseup={() => cursorHandler(2,1)}><span class="left-mark">▲</span></button>
<button class="position-center btn cross-key-btn"><span class="center-mark">●</span></button>
<button class="position-right btn cross-key-btn"
on:mousedown={() => cursorHandler(3,0)}
on:mouseup={() => cursorHandler(3,1)}><span class="right-mark">▲</span></button>
<button class="position-bottom btn cross-key-btn"
on:mousedown={() => cursorHandler(4,0)}
on:mouseup={() => cursorHandler(4,1)}><span class="bottom-mark">▲</span></button>
</div>
</div>
<div class="controller-center">
<div class="selectstart-btn-set">
<button class="btn selectstart-btn"
on:mousedown={() => btnSelectHandler(0)}
on:mouseup={() => btnSelectHandler(1)}></button>
<button class="btn selectstart-btn"
on:mousedown={() => btnStartHandler(0)}
on:mouseup={() => btnStartHandler(1)}></button>
</div>
</div>
<div class="controller-right">
<div class="abxy-btn-set">
<div class="cross-layout">
<button class="btn abxy-btn position-top btn-x"
on:mousedown={() => btnXHandler(0)}
on:mouseup={() => btnXHandler(1)}>X</button>
<button class="btn abxy-btn position-left btn-y"
on:mousedown={() => btnYHandler(0)}
on:mouseup={() => btnYHandler(1)}>Y</button>
<button class="btn abxy-btn position-right btn-a"
on:mousedown={() => btnAHandler(0)}
on:mouseup={() => btnAHandler(1)}>A</button>
<button class="btn abxy-btn position-bottom btn-b"
on:mousedown={() => btnBHandler(0)}
on:mouseup={() => btnBHandler(1)}>B</button>
</div>
</div>
</div>
</div>
<style lang="scss">
.spf-controller {
display: flex;
flex-direction: row;
justify-content: center;
.btn {
border-style: none;
cursor: pointer;
}
.cross-layout {
display: grid;
grid-template-columns: 30px 30px 30px;
grid-template-rows: 30px 30px 30px;
.position-top {
grid-row: 1 / 2;
grid-column: 2 / 3;
}
.position-left {
grid-row: 2 / 3;
grid-column: 1 / 2;
}
.position-center {
grid-row: 2 / 3;
grid-column: 2 / 3;
}
.position-right {
grid-row: 2 / 3;
grid-column: 3/4;
}
.position-bottom {
grid-row: 3 / 4;
grid-column: 2/3;
}
.abxy-btn {
border-radius: 50%;
width: 30px;
height: 30px;
color: white;
&.btn-x {
background-color: blue;
&:active {
color: black;
background-color: rgb(125, 208, 255);
}
}
&.btn-y {
background-color: green;
&:active {
color: black;
background-color: rgb(185, 243, 185);
}
}
&.btn-a {
background-color: red;
&:active {
color: black;
background-color: rgb(255, 179, 179);
}
}
&.btn-b {
background-color: yellow;
&:active {
color: black;
background-color: rgb(255, 255, 154);
}
}
}
.cross-key-btn {
width: 30px;
height: 30px;
background-color: rgba(66, 86, 123, 0.5);
&:active {
color: aliceblue;
background-color: red;
}
.left-mark {
display: block;
transform: rotate(-90deg);
}
.right-mark {
display: block;
transform: rotate(90deg);
}
.bottom-mark {
display: block;
transform: rotate(180deg);
}
}
}
.controller-right {
z-index: 2;
display: inline-block;
background-color: rgb(229, 227, 250);
padding: 20px;
border-radius: 50%;
.abxy-btn-set {
display: inline-block;
padding: 10px;
background-color: rgba(66, 86, 123, 0.5);
border-radius: 50%;
}
}
.controller-left {
z-index: 2;
display: inline-block;
background-color: rgb(229, 227, 250);
padding: 30px;
border-radius: 50%;
}
.controller-center {
z-index:1;
width: 220px;
height: 110px;
background-color: rgb(229, 227, 250);
display: block;
margin: auto;
margin-top: 0;
margin-left: -80px;
margin-right: -80px;
padding-top: 20px;
text-align: center;
font-size: 0.8em;
color: gray;
.selectstart-btn-set {
display: block;
width: 140px;
text-align: center;
margin: auto;
margin-top: 50px;
.selectstart-btn {
width: 10px;
height: 35px;
border-radius: 15%;
background-color: rgba(66, 86, 123, 0.5);
transform: rotate(30deg);
&:first-child {
margin-right: 20px;
}
&:active {
background-color: rgb(255, 115, 0);
}
}
}
}
}
</style>
なお、素のデザインなしのコントローラーでも良かったのですが、せっかくですので以下のサイトで紹介されている「スーパーファミコン風コントローラー」のスタイルを参考にレトロな雰囲気にさせていただきました。
このSvelteコンポーネントの実装で重要なことは、
上のソースコードでは少し長いので、重要なところだけを以下のように抜き出してみます。
<script lang="ts">
export let cursorHandler: (dir: number, state: number) => void;
//...中略
</script>
//...中略
<button class="position-top btn cross-key-btn"
on:mousedown={() => cursorHandler(1,0)}
on:mouseup={() => cursorHandler(1,1)}><span class="top-mark">▲</span></button>
//...以降略
今回で言えば、ゲームのメインエンジンである
GameStage.svelte
controller.svelte
子コンポーネント上でイベントが発生した場合、親コンポーネント側で用意した関数(ハンドラ)を子に渡すことで、コンポーネント間のイベントの共有が可能です。
上の例でいうと、子コンポーネント側に
export let cursorHandler ...
そして、親コンポーネントにハンドラ(
cursorHandler
ということで、
GameStage.svelte
<script lang="ts">
//...中略
//👇Controllerコンポーネントを追加
import Controller from '../components/controller.svelte';
//...中略
//👇各ボタンに対応したハンドラを追加
function onClickCrossCursor(direct: number, state: number) {
if (state === 0) {
if (movingLock) {return;}
switch(direct) {
case 1:
key = 'u';
break;
case 4:
key = 'd';
break;
case 2:
key = 'l';
break;
case 3:
key = 'r';
break;
default:
break;
}
if (key != oldKey) {
movingFlg = key;
movePlayer();
}
oldKey = key;
}
else if (state === 1) {
key = 'n';
if (!movingLock) {movePlayer();}
oldKey = key;
}
}
function onClickABtn(state: number) {
console.log('A', state);
}
function onClickBBtn(state: number) {
console.log('B', state);
}
function onClickXBtn(state: number) {
console.log('X', state);
}
function onClickYBtn(state: number) {
console.log('Y', state);
}
function onClickStartBtn(state: number) {
console.log('START', state);
}
function onClickSelectBtn(state: number) {
console.log('SELECT', state);
}
//...中略
</script>
//...中略
//👇HTML部にControllerコンポーネントを追加
// ... 各ボタンに対応したイベントハンドラも設定
<Controller
cursorHandler={onClickCrossCursor}
btnAHandler={onClickABtn}
btnBHandler={onClickBBtn}
btnXHandler={onClickXBtn}
btnYHandler={onClickYBtn}
btnStartHandler={onClickStartBtn}
btnSelectHandler={onClickSelectBtn}></Controller>
//...以下略
Svelteでコンポーネント間でイベントを共有するテクニックはいくつかあり、
bind:this
document.getElementById
on:{イベント}属性
Svelteコンポーネントを右クリック禁止にする
細かい修正として、コンポーネントをマウスクリックする際に、通常のブラウザの挙動のままだと、

この右クリックしたらメニューが表示されるのをコンポーネント単位で避けるためには、SvelteコンポーネントのDOM要素に以下のような属性を記述するだけでOKです。
<div class="hoge" on:contextmenu|preventDefault>
...
</div>
Svelteの場合、
on:contextmenu|preventDefault

Svelteコンポーネントでマウス操作中のテキスト選択を無効にする
もう一つHTMLゲームを作るときに気をつけておきたいのは、マウスでテキスト文字を長押ししてみたり、テキスト上をマウスでドラッグしてみたりするような
ブラウザでウェブページを閲覧するときに、文章をマウスドラッグで選んでコピーするなど、普通は無いと不便です。
他方、HTMLベースのゲームとなると、一々キャラクターのセリフに表示させるテキストが選択できてしまうことは問題になります。
ということで、ゲーム中のテキストは基本的に選択させない、という対策も必要です。
一つは、マウス操作の開始時に起こるイベント・
onselectstart
例えばゲーム全体を新しく
div
「on:selectstart|preventDefault」
<div id="wrapper" on:selectstart|preventDefault>
...
</div>
実際にこの対策の前後で、ゲーム中のテキストをマウスで操作してみると、

というようにテキストは選択できないようになります。
代替手段としては、cssスタイルからテキストを選択することもできます。
先程の
onselectstart
例えば、ゲーム全体をテキスト禁止にしたい場合、ルートのスタイルファイルの
app.css
app.scss
//...
#app {
margin: 0 auto;
padding: 12px;
//👇追加
user-select: none;
}
これもマウスによる要素中のテキストコンテンツをまるごと選択禁止にすることができます。
ブラウザによっては
onselectstart
user-select: none;
不安なら両方とも対策しておけば良いでしょう。
iframeのリソースキャッシュを無効化するメタタグ
こちらはSvelteアプリとは直接関係はないのですが、最終的にSvelteアプリをウェブページの
iframe
他のブログようなブラウザの画面の更新頻度がほとんどない用途とは異なり、HTMLブラウザゲームは非常に短い頻度で画像の生成や描画が切り替わります。
標準のブラウザでは、読み込んだ画像やテキストデータ等はキャッシュとして裏で取り溜めてくれる機能が働いてくれるので、一度読み込んだウェブページにあるリソースを再度サーバーから取得する手間が省けてより高速に描画することができます。
これがゲームアプリとなると、ブラウザ内部で常時計算処理されて生成される画像を高頻度で書き換えてブラウザに表示させる際に、キャッシュ機能が有効になっていることで描画が更新されないなど、おかしな挙動をする可能性があります。
ということで、iframeでHTMLアプリをキャッシュなしで動作させるときのおまじないとして、
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
...略
<!-- 👇以下の3つのタグでブラウザキャッシュを無効化 -->
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
...略
</head>
...略
ただし、jsファイルなどのスクリプトファイルはこの設定でもキャッシュしてしまうようなので、スクリプトの読み込みURL値にハッシュを組み込んだクエリ名を使うなどもうひと工夫必要となります。
まとめ
以上、今回はSvelteでゲームコントローラーを模したコンポーネントを作成して、スプライトを操作する方法を紹介してみました。
この記事で説明したポイントを再掲すると、
1. 親子関係にあるSvelteコンポーネント間のイベントの渡し方
2. ゲーム画面上のコンポーネントを右クリック禁止にする
3. ゲーム画面上の文字テキストの選択を禁止にする
4. iframeのリソースキャッシュを無効化するメタタグの書き方
ということでした。
今回の記事が理解できれば、Svelteゲームの開発の基本的な流れって、
Svelteに限らずJavascriptを使ったプロの方が教えてくれる動画講座・「
次回は、RPG風のレトロな会話ウィンドウの機能を追加してみようと予定しています。