svelte.jsでゲームをつくってみた

# この記事の概要

`svelte.js`で間違い探しゲームをつくってみました。いろいろとハマったポイントもあったので忘れないようにメモしていきます。

# 作ったモノの概要

ピクトグラムを使った間違い探しゲームです。

![svelte_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/b0971132-18a2-31e8-fce1-70cab9dc1df8.gif)

チュートリアルも実装されています。

![svelte_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/bcc786df-7ba7-7b60-f84a-11e65ee5969a.gif)

# 公開先

- ソース
  - https://gitlab.com/tamoco-mocomoco/svelte-de-find-diff/

- 公開先URL（実際に遊べます）
  - https://svelte-de-find-diff.netlify.app/

# 使ったフレームワーク

- svelte.js (JavaScript全体のフレームワーク)
- driver.js (チュートリアル用のライブラリ)
- bulma (CSSのフレームワーク)

# svelte.jsを使って困ったこと

## npm installで他のパッケージが上手くインストールできない

webpackあたりでエラーが出てインストールできなかったモノがあったので、一旦は`index.html`に`CDN`で利用するようにしました。ここらへんはもう少し調査が必要そうです。今回は作ることを優先したので一旦`CDN`で利用する形を取りました。

## 配列を追加しても再描画されない

公式のドキュメントにも書いていますが配列を追加しただけだと画面上のオブジェクトが更新されません。
ログで出力するとちゃんと変数に入っているので、結構ココでハマるポイントかもしれません。
それじゃどうすればいいのかというと、変数として再代入することで解決します。`svelte.js`の仕様上、変数に再代入されたときだけちゃんと再描画として認識するようです。`vue.js`で慣れていると「ナンデダヨ！！」っていう部分です。

- 参考サイト
  - https://svelte.dev/tutorial/updating-arrays-and-objects
     - 以下のnumberにプッシュしてからの再代入がなんとも・・・
     - numbers.push(numbers.length + 1);
     - numbers = numbers;

ちなみに今回のソースで該当の部分は以下のとおり

```html
<script>
    export const changeImage = () => {
        setAnimation();
        imgArray = [];
        const appearRnd = Math.ceil(Math.random() * appearImg) - 1;
        const imageRnd = Math.ceil(Math.random() * imageCount);
        for (let step = 0; step < appearImg; step++) {
            if (appearRnd == step) {
                // imgArray.push(`pict_img/${imageRnd}-A.png`);
                // ※pushの場合は変数に再代入する必要がある
                // imgArray = imgArray;
                imgArray = [...imgArray, `pict_img/${imageRnd}-A.png`];
            } else {
                // imgArray.push(`pict_img/${imageRnd}-B.png`);
                imgArray = [...imgArray, `pict_img/${imageRnd}-B.png`];
            }
        }
    };
</script>

{#if isVisible}
    <div transition:fly={{ y: 100 }} class="container has-text-centered">
        {#each imgArray as img}
            <BtnPict imagePath={img} clickFunc={clickAction} />
        {/each}
    </div>
{/if}
```

## 使う機能をimportする必要がある

`vue.js`だと初期で読み込む`created`や`mounted`などはimportせずに利用できます。`svelte.js`の場合は以下のように、その都度読み込む必要があるため面倒です。もしかすると上手くやる方法があるかもしれません。

```html
<script>
    // onMountを使うなら毎回宣言する必要がある
    import { onMount } from "svelte";
</script>
```

## 動的にon:clickに対して関数を設定するときに注意が必要

`on:click`に関数を設定するときにそのまま設定すると死にます。引数がない場合は`()`なしで設定すればよいのですが、引数付きの関数の場合は気をつける必要があります。

以下はダメパターン。

```html
<script>
    export let clickFunc = (msg) => console.log(msg);
</script>

<p on:click={clickFunc ('コンソールログがタイヘンなことに・・・')}>TEST</p>
```

以下はイイパターン。

```html
<script>
    export let clickFunc = (msg) => console.log(msg);
</script>

<p on:click={() => {clickFunc ('ヤッホー')} }>TEST</p>
```


# svelte.jsを使ってよかったこと

## 変数の扱いがわかりやすい
`vue.js`に比べてそのまま`JavaScript`の変数を扱うような感じです。具体的にいうと`vue.js`の場合はテンプレートに渡す変数は`props`として渡す必要があります。そして`props`から渡ってきた変数はそのまま再代入すると`warning`で怒られます。`data`で変数を宣言しなおして再代入することが正しいわけです。

`vue.js`ですが以下はwarningで怒られます。

```javascript
export default {
  props: {
    propsValue: Number
  },
  methods: {
    editValue() {
      this.propsValue = 20
    }
}
```

`vue.js`では以下が正しい姿

```javascript
export default {
  props: {
    propsValue: Number
  },
  data() {
    // プロパティの値を設定
    dataValue: this.propsValue
  },
  methods: {
    editValue() {
      // データに定義した値を変更
      this.dataValue = 20
    }
}
```

ここまで`vue.js`のことを書いてきましたが、`props`や`data()`でそれぞれの変数の扱いがありますが、`svelte.js`だとそういった扱いの違いがなく直感的に扱えるのは良い点だと思いました。


`svelte.js`で違うコンポーネントに渡したい場合は・・・


■ App.svelte

```html
<script>
    import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
```

`export`をつけるだけで渡せます。

■ Nested.svelte

```html
<script>
    export let answer;
</script>

<p>The answer is {answer}</p>
```

## 読み取り専用のストアの変数を設定できる

上記のようにコンポネントで変数を渡し合うことでどこで何の変数が使われているか把握しづらくなります。１対１で渡すだけならいいのですが、コンポネントに渡してその先のコンポネントに渡してのバケツリレーになったりすると悲惨です。

そのためにアプリケーションで扱うデータセットを`store`と呼ばれる領域で一元管理することで、各コンポーネントは`store`にアクセスすれば常に共通の値を参照する仕組みがあります。これは`vue.js`でも同様でいろいろなコンポネントから値を参照する場合に有効だったりします。

ここでも`vue.js`の話になりますが、`vue.js`の場合は`store`に`getter`や`setter`を記述して、`store`の変数を参照したいときは`getter`を使い、`store`の変数を更新したいときは`setter`を使うわけです。

`svelte.js`でも`store`を使えるわけですがよりわかりやすく使用できます。

■store.js

```javascript
import { writable } from 'svelte/store';
export const ST_score = writable(0);
```

■App.js

```html
<script>
    import { ST_score, ST_gamestart } from "./stores.js";
    import { get } from "svelte/store";
    //更新
    ST_score.set(100);

    //他のコンポネントから更新されたか検知したい場合はsubscribeを利用
    let score = 0;
    ST_score.subscribe((value) => {
        score = value;
    });

    //ちなみにsvelte/storeのgetでも取得はできる
    //（ただし他での更新は検知されない）
    //初期値を代入するときに利用できそう
    console.log(get(ST_score));

</script>
```

ただし扱いやすくなる反面、使われたくないタイミングで使われるケースもあるわけです。例えば、変なところで変数を更新されたくないため、読み取り専用にしたいな～というケースもあるわけです。そこで`svelte.js`ではJavaとかでいうところのカプセル化のような隠蔽化するような仕組みもあります。

今回の作ってみたモノではハイスコアを読み取り専用にしてスコアの状態からハイスコアを更新するようにしています。ちなみにハイスコアはローカルストレージに保存するようにしています。

```javascript
import { readable, writable } from 'svelte/store';
import { get } from 'svelte/store';

const lsKey_hiScore = "hiscore"
let lsHiScore = localStorage.getItem(lsKey_hiScore);
lsHiScore = lsHiScore ? lsHiScore : 0;

export const ST_score = writable(0);
export const ST_hiScore = readable(null, function start(set) {
    //readableで初期値をセットできる
    set(lsHiScore);

    //この中だけで値の更新ができる
    //スコアの変更を検知して条件によって更新する
    ST_score.subscribe(value => {
        console.log('hi-score: ' + get(ST_score));
        if (lsHiScore < value) {
            set(value);
            localStorage.setItem(lsKey_hiScore, value);
        }
    });
});
```

## 最初から使えるトランジションが便利（効果や演出がラク）

フェードインやスクロールインのアニメーションなどカンタンに使えます。
時間の設定なども細かくできるので他のCSSのライブラリとか使わずアニメーションを設定出来るのは便利かなと。
ただし、使うトランジションごとにインポートは必要で、コンポネント各所で利用すると思ったとおりの演出にならないこともあるので、イイカンジにお付き合いする必要があります。
実際にトランジションを使う場合は、`if`などで切り替わることでトランジションを発生させることができるようです。もしも初回の表示でトランジションを使いたい場合は、`onMount`などで切り替えるとよさそうです。




```html
<script>
    import { onMount } from "svelte";
    import { fly } from "svelte/transition";
    let isVisible = false;

    onMount(() => {
        let isVisible = true;
    });
</script>

{#if isVisible}
    <p transition:fly={{ y: 100 }}>
       下から上にせり上がる感じのやーつ 
    </p>
{/if}
```

# 総評

最初こそはいろいろと`vue.js`との違いで文句があったりしましたが、あとになればなるほどサクサク書けるようになりました。
`store`による`readable`ですが設定することで他の`store`の`変数`をトリガーにする形にしているので面倒くさいことになったかなと。
あとは１つのコンポネントが大きくなってきたら細かく分けたり、ここらへんは`vue.js`と同様のセンスが必要だったりします。
あとはチュートリアルのライブラリがかゆいところに手が届かなくて試行錯誤したのが苦労したところかなと思います。