Vue.jsでドラクエ風のポートフォリオを作った話　①

# TL;DR

* Vue.jsを使ってドラクエ風ポートフォリオを作りました
* できあがったものが[こちら](https://iwathi.netlify.com/)
* GitHubは[こちら](https://github.com/iwathi/dq_portfolio) ← New

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ドラクエ風ポートフォリオサイトが一応できた。（レスポンシブ未対応）<br><br>なおポートフォリオの役目を果たしているかは不明なので、正しくは<br>「ドラクエ風ポートフォリオ風サイト」<a href="https://t.co/atlgBcB9B0">https://t.co/atlgBcB9B0</a> <a href="https://twitter.com/hashtag/%E3%83%9D%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%AA%E3%82%AA?src=hash&amp;ref_src=twsrc%5Etfw">#ポートフォリオ</a><a href="https://twitter.com/hashtag/%E3%83%89%E3%83%A9%E3%82%AF%E3%82%A8?src=hash&amp;ref_src=twsrc%5Etfw">#ドラクエ</a> <a href="https://t.co/yJ45hZGH7c">pic.twitter.com/yJ45hZGH7c</a></p>&mdash; iwathi (@iwathi) <a href="https://twitter.com/iwathi/status/1060464401533161472?ref_src=twsrc%5Etfw">November 8, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# あらすじ

　前からポートフォリオを作ろうと思っていたのですが、普通に作るだけでは芸がないかと思い、「ドラクエ風RPG」をイメージして作っています。

　フロントエンドの実務経験がないので、実装期間は10日間（約2時間/日）ほどでした。

# できたもの

<img width="540" alt="しらべる.png" src="https://qiita-image-store.s3.amazonaws.com/0/170821/b1a98cf3-e34f-927b-2e6b-b70a51f01488.png">

[ドラクエ風ポートフォリオ](https://iwathi.netlify.com/)

　ちょっと触っていただければ分かると思うのですがマウスやタッチ操作でキャラクターを動かし、人に話しかけたりたからばこを調べたりできます。

　ドラクエと一緒で場所によってできることが変わるので色々動かしてみてください。

　なお、レスポンシブに非対応だったり、ポートフォリオとしては色々情報が足りていなかったりしますが、そのへんは随時更新していこうと思っています。

GitHubに関してはあとで公開する予定です。

[](https://github.com/iwathi/portfolio_dq)

# 使ったもの
* Vue.js
* Nuxt.js
* Vuex

　フロントエンドを勉強したいと思い、フロントエンド最前線の友達に聞いたところ「フレームワークは好みだけど、俺はVue.jsを使ってる。公式のドキュメントがスーパーわかりやすい！」と絶賛だったのでスーパー分かりやすいのを私も選ぶことにしました。

# 構成

　画面の各要素を個別のコンポーネントとして分けて実装をしました。それ同士をVuexで状態管理して協調動作させているような形になります。

<img width="545" alt="各コンポーネント.png" src="https://qiita-image-store.s3.amazonaws.com/0/170821/dbc0d67a-cad0-9ed4-fa54-6911c6b751f4.png">

### 各コンポーネントの役割

#### ①TheStatus.vue

ドラクエと同じくキャラクターのステータスを表示
 
#### ②TheCommand.vue

今使えるコマンドを表示
イベントによってコマンドは増減

#### ③index.vue

キャラクターの移動範囲を表示
クリックで移動

#### ④TheComment.vue

コマンドに合わせたメッセージを表示
状態によってメッセージは変動

#### ⑤TheChoice.vue

メッセージ内で選択肢がある場合に表示
選択によってメッセージが変動

#### ⑥⑦⑧

また、つよさ・さくせん、などもまた別コンポーネントで構成されています。

# 今回説明する内容

　全てを一気に説明するのは厳しいので、今回は下記2点に内容を絞って説明していきます。

* ステータスの表示
* キャラクターの移動

## ステータスの表示

一定のイベント毎にstateExが加算され、それによってstateLevelが変動します。

　経験値とキャラクターのレベルの関係が比例していて、ドラクエの職業的な部分を「らいほうしゃ」から「ともだち」に変化していく様な作りにしています。

　経験値を増やしていくとより地位の高いポートフォリオ閲覧者になることができます。

### TheStatus.vue 

 TheStatus.vueの中身を分割して説明していきます。

```vue

<template>
  <section>
    <div class="status-list">
      <ul>
        <li>{{levelList[statusLevel].job}}</li>
        <li>レベル：{{statusLevel}}</li>
        <li>ＨＰ：{{levelList[statusLevel].hp}}</li>
        <li>ＭＰ：{{levelList[statusLevel].mp}}</li>
        <li>ＥＸ：{{stateEx}}</li>
      </ul>
    </div>
  </section>
</template>
```

　レベルに応じて表示の内容を変更するためにバインディングをして、dataの内容をリアクティブに表示をしています。

[Vueガイド: データバインディング構文](https://v1-jp.vuejs.org/guide/syntax.html)

```vue
<script>

export default {
  data (){
    return {
      levelList: [
        {
          job: 'らいほうしゃ',
          hp: '1',
          mp: '1',
          ex: 1
        },
        {
          job: 'ともだち',
          hp: '100',
          mp: '100',
          ex: 20
        },
       //（略）//
      ]
    }
  },

  //（略）//
</script>
```

　レベル毎の職業はリスト型で、exが経験値の閾値となっています。

```vue
<script>
  //（略）//
  computed: {
    stateEx: function(){
      return this.$store.state.ex
    },
    statusLevel: function(){
      var tmpLevel = 0
      for (var i = 0; this.levelList.length > i ; i++){
        if (this.stateEx > this.levelList[i].ex){
          tmpLevel = i
        }
      }
      return tmpLevel
    }
  }
}
</script>
```

　computedの中身です。

　stateExの経験値の部分は他のコンポーネントのイベントで変動するのでVuexを使って値をストアしています。

　Vuexにストアされた値を使う際はcomputedで取得することでTheStatus.vueのコンポーネントの中で宣言した変数の様に使用することができるようです。

　また、経験値によってレベルを変化させたい（閾値によってレベル判断をしたい）等のロジックを反映させた数値を使用したい場合にも、computedを使用しますので、statusLevelとして宣言を行っています。

[Vueガイド: 算出プロパティ computed](https://v1-jp.vuejs.org/api/#computed)

```vue

<style>

.status-list{
  text-align: center;
  width: 130px;
  height: 120px;
  padding:10px;
  margin-bottom:10px;
  border: 2px solid #fff;
  border-radius: 10px;
}
</style>
```

　TheStatus.vueのCSSの中では枠線や文字の位置等の調整のみをおこなっています。

　「げすとさん」といった文字やステータスの位置自体はlayoutsのdefault.vueで指定をしています。

## キャラクターの移動

 こちらの記事を参考にさせていただきました。

[vue.jsでcsvファイルのデータ抽出表示とオセロ](https://qiita.com/tantai/items/5cf98364efa4a80324bd)

　この記事の中でオセロを置くために使われている実装方法をキャラクターの移動に利用しています。

### index.vue

```vue
<template>
  <section>
  <div class="map">
    <div v-for="i in 8" class="container" :key="i">
      <div v-for="j in 9" class="waku" :key="j">
        <div :class="{ 'object-human': xy[i][j]==1, 'visitor': xy[i][j]==0 }" @click="visitorMove(i, j)">
          <img class="visitor" v-if="xy[i][j]==0" src="../assets/img/brave.png"/>
          <img class="object-treasure" v-if="xy[i][j]==4" src="../assets/img/object-treasure-1.png"/>
          <img class="object-treasure" v-if="xy[i][j]==5" src="../assets/img/object-treasure-2.png"/>
          <img class="object-human" v-if="xy[i][j]==1" src="../assets/img/tono.png"/>
          <div class="object-wall" v-if="xy[i][j]==9"></div>
        </div>
      </div>
    </div>
  </div>
  </section>
</template>
```

　dataにあるxyという2次元配列がマップを表しています。それぞれの数値には下記のような意味を持たせており、変更することでマップに対してオブジェクトを設置することができます。

0: キャラクターの位置
1: 人オブジェクトの位置
2: 床オブジェクト(キャラクター移動可能)
3: マップ外
4: 宝箱オブジェクト①
5: 宝箱オブジェクト②
9: 壁オブジェクト

 v-forとv-ifを使い上記の配列を使用しています。

```vue
<script>

export default {
  data: function() {
    return {
      wb: 0,
      xx: 8,
      yy: 5,
      xy: [
        [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
        [3, 9, 9, 9, 9, 9, 9, 9, 9, 9, 3],
        [3, 9, 4, 9, 2, 1, 2, 9, 5, 9, 3],
        [3, 9, 2, 2, 2, 2, 2, 2, 2, 9, 3],
        [3, 9, 2, 2, 2, 2, 2, 2, 2, 9, 3],
        [3, 9, 2, 2, 2, 2, 2, 2, 2, 9, 3],
        [3, 9, 2, 2, 2, 2, 2, 2, 2, 9, 3],
        [3, 9, 2, 2, 2, 2, 2, 2, 2, 9, 3],
        [3, 9, 2, 2, 2, 0, 2, 2, 2, 9, 3],
        [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
      ],
    }
  },
  methods: {
    visitorMove: function(x, y) {
      //置ける場所の判定
      if (this.xy[x][y] != 2) return;
      this.xy[x][y] = this.wb;
      this.xy[this.xx][this.yy] = 2;
      this.xx = x;
      this.yy = y;
      this.xy.push();

      this.setPositionState()
    },
    // left x, right x, forward y, back y
    setPositionState: function() {
      const lx = this.xy[this.xx - 1][this.yy]
      const rx = this.xy[this.xx + 1][this.yy]
      const fy = this.xy[this.xx][this.yy - 1]
      const by = this.xy[this.xx][this.yy + 1]

      this.$store.commit('setlx', lx)
      this.$store.commit('setrx', rx)
      this.$store.commit('setfy', fy)
      this.$store.commit('setby', by)
    }
  }
}
</script>
```

```vue
<style>
p {
  text-align: center;
}

div {
  min-width: 40px;
  min-height: 40px;
}

.map {
  background-image: url("../assets/img/tile-1.png");
}
.container {
  display: flex;
  justify-content: center;
}

.waku {
  width: 40px;
  height: 40px;
}

.object-treasure {
  padding-top: 2px;
  padding-left: 2px;
  width: 90%;
  height: 90%;
}

.object-tile {
  width: 100%;
  height: 100%;
}

.object-wall {
  background-image: url("../assets/img/wall.png");
  background-repeat: no-repeat;
  background-size: cover;
  width: 100%;
  height: 100%;
}


.object-human {
  padding-top: 2px;
  padding-left: 2px;
  width: 90%;
  height: 90%;
}

.visitor {
  width: 100%;
  height: 100%;
  object-position: 100% 100%;
  object-fit: cover;
}

.pages-top{
  width: 50vw;
  height: 300px;
  padding:10px;
  margin-bottom:10px;
  border: 2px solid #fff;
  border-radius: 10px;
}
</style>
```

# とりあえずここまで

　とりあえず今回はステータスの部分とキャラクターの移動の部分の説明のみをおこないました。

　次回の投稿で他の部分の実装の説明もしていきたいと思います。

# 追加(2018.11.17)

フォントをPixelMplusに変更してみました！