JavaScript
riot
riot.js

Riot.jsで蛇のゲーム

背景

個人的にSVGに興味が出てきた+ゲームを作りたかったので作ってみた。

完成したものは下記
http://hasito.com/snake/

仕様

  • 2Dゲーム
  • 蛇が餌を食べると長くなる
  • 壁/体にあたったら死ぬ
  • 少しづつ動きが早くなる
  • キーボードで操作
    こんな感じ…

開発

枠を組む

SVGで下記のように書くと格子状にブロックを配置できる様子

    <svg width="30" height="60">
      <g transform={('translate(0,0)')}>
        <rect x="0" y="0" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="15" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="30" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="45" width="15" height="15" title=""  fill="#000000"></rect>
      </g>
      <g transform={('translate(15,0)')}>
        <rect x="0" y="0" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="15" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="30" width="15" height="15" title=""  fill="#000000"></rect>
        <rect x="0" y="45" width="15" height="15" title=""  fill="#000000"></rect>
      </g>
    </svg>

これをriot.jsで記載

    <svg riot-width={ w*15 } riot-height={ h*15 } >
      <g each={d,i in view} transform={('translate('+(i*15)+',0)')}>
        <rect each={d2,i2 in d} x="0" riot-y={i2*15} data-point='{(JSON.stringify({x:i,y:i2}))}' width="15" height="15" title=""  fill="#{('000000'+d2.toString(16)).slice(-6)}"></rect>
      </g>
    </svg>

注意点

  • viewには多次元配列が入っている予定
  • fillは16進指定。6桁必要なので0で頭をうめています。
  • xやらwidthは頭にriot-をつける必要あり

全体の流れ

全体の処理の流れとしては下記な感じを想定して作りました。
1. 初期化処理
2. メイン処理
3. 蛇の死
4. 初期化処理(2回目)
5. メイン処理(2回目)
…こんな感じ

初期化処理

    /* タイマーセット関数(一定ごとに時を進める用)
    */
    var tm_set=()=>{
      if(self.tme)clearInterval(self.tme);
      self.tme=setInterval(()=>{
        self.tm-=1;
        tm_set2(self.tm);
      },1000);//とりあえず1秒単位で更新固定
    };
    /* タイマーセット関数(メイン処理用)
    */
    var tm_set2=(v)=>{
      if(self.tme2)clearInterval(self.tme2);
      self.tme2=setInterval(()=>{
        _main_();
      },v);
    };
    /* 初期化処理
    */
    var _init_=()=>{
// 変数の初期化
      self.st=1;//ゲームステータス
      self.w=40;//横の長さ
      self.h=40;//縦の長さ
      self.view=new Array(self.h);//表示用変数初期化
      for(i=0;i<self.h;i++){
        self.view[i] = new Array(self.w);
        self.view[i].fill(0);
      }
      self.map=new Array(self.h);//マップ変数の初期化(今は餌しか置かれない…)
      for(i=0;i<self.h;i++){
        self.map[i] = new Array(self.w);
        self.map[i].fill(0);
      }
      self.snake={//蛇の初期化
        hed:{x:Math.floor(self.w/2),y:Math.floor(self.h/2)},//頭の位置
        body:[],//体
        point:0,//ポイント
        move:{x:1,y:0}//移動属性(キーダウンイベントで変化)
      }
      self.tm=300;//時間更新間隔(初期値)
//キーダウンイベント
      document.onkeydown = function (e){
        var k = e.keyCode;
        var s = self.snake;
        console.log(k)
        if(k==37)s.move={x: 0,y:-1};//左
        if(k==38)s.move={x:-1,y: 0};//上
        if(k==39)s.move={x: 0,y: 1};//右
        if(k==40)s.move={x: 1,y: 0};//下
        if(k==32)self.st=2;//game開始
      };
//タイマー設定
      tm_set();
      console.log("_init_");
    }

移動処理

    /* run処理
    */
    var _run_=()=>{
      var s = self.snake;
      // --move-- 移動処理
      // body
      s.body.unshift(JSON.parse(JSON.stringify(s.hed)));
      s.body.pop();
      // hed
      s.hed.x += s.move.x;
      s.hed.y += s.move.y;
      // --dead-- 死亡判定
      var b1 = (s.hed.x<self.w);
      var b2 = (s.hed.y<self.h);
      var b3 = (s.hed.x>=0);
      var b4 = (s.hed.y>=0);
      var b5 = (s.body.filter((v)=>{return (s.hed.x==v.x)&&(s.hed.y==v.y);}).length==0);
      if(!(b1&&b2&&b3&&b4&&b5)){
        return false;
      }
      // --eat-- お食事判定
      if(self.map[s.hed.y][s.hed.x]=="food"){
        if(s.body.length==0){
          s.body.push(JSON.parse(JSON.stringify(s.hed)));
        }else{
          s.body.push(JSON.parse(JSON.stringify(s.body[s.body.length-1])));
        }
        s.point+=1/self.tm;
      }
      self.map[s.hed.y][s.hed.x]=0;
      // --food-- 餌をRANDOMに置く処理
      var foody=Math.floor(Math.random()*self.h);
      var foodx=Math.floor(Math.random()*self.w);
      var fb1=(s.body.filter((v)=>{return (foodx==v.x)==(foody==v.y);}).length==0);
      var fb2=(!((s.hed.x==foodx)&&(s.hed.y==foody)));
      if(fb1&&fb1){
        self.map[foody][foodx]="food";
      }
      // --view-- 表示を更新する処理
      for(i=0;i<self.h;i++){self.view[i].fill(0);}
      self.view[s.hed.y][s.hed.x]=0x0000ff;
      s.body.forEach((v)=>{
        self.view[v.y][v.x]=0x5555ff;
      });
      self.map.forEach((y,yi)=>{
        y.forEach((x,xi)=>{
          if(x=="food"){
            self.view[yi][xi]=0xff00ff;
          }
        });        
      })

      return true;
    }


メイン処理

ほぼ、切り替えしかしてないけど…

    /* main
    */
    var _main_=()=>{
      if(self.st==2){//st:0>初期 1>停止 2>ゲーム中
        if(!_run_()){
          _init_();
        }
        self.update();
      }
    };

全体コード

index.html

<html>
  <head>
    <title>Hello Riot.</title>
    <meta charset="UTF-8"/>
  </head>
  <body>
    <sample></sample>
    <script type="riot/tag" src="sample.tag"></script>
   <!-- <script src="https://cdn.jsdelivr.net/npm/riot@3.9/riot+compiler.min.js"></script>  -->
   <script src="riot/riot+compiler.min.js"></script>
    <script>riot.mount('sample')</script>
  </body>
</html>

sample.tag

<sample>
    <h1>蛇のやつ</h1>
    <h3>スペースキーで開始してください</h3>
    <svg riot-width={ w*15 } riot-height={ h*15 } >
      <g each={d,i in view} transform={('translate('+(i*15)+',0)')}>
        <rect each={d2,i2 in d} x="0" riot-y={i2*15} data-point='{(JSON.stringify({x:i,y:i2}))}' width="15" height="15" title=""  fill="#{('000000'+d2.toString(16)).slice(-6)}"></rect>
      </g>
    </svg>
    <h3>ポイント{snake.point}点</h3>
    <h3>体の長さ{snake.body.length}ブロック</h3>
    <h3>頭の位置{snake.hed.x}:{snake.hed.y}</h3>
  <script>
    var self=this;
    self.st=0;
    self.tme=false;
    self.tme2=false;
    /* タイマーセット関数(一定ごとに時を進める用)
    */
    var tm_set=()=>{
      if(self.tme)clearInterval(self.tme);
      self.tme=setInterval(()=>{
        self.tm-=1;
        tm_set2(self.tm);
      },1000);
    };
    /* タイマーセット関数(メイン処理用)
    */
    var tm_set2=(v)=>{
      if(self.tme2)clearInterval(self.tme2);
      self.tme2=setInterval(()=>{
        _main_();
      },v);
    };
    /* main
    */
    var _main_=()=>{
      if(self.st==2){
        if(!_run_()){
          _init_();
        }
        self.update();
      }
    };
    /* run処理
    */
    var _run_=()=>{
      var s = self.snake;
      // --move--
      // body
      s.body.unshift(JSON.parse(JSON.stringify(s.hed)));
      s.body.pop();
      // hed
      s.hed.x += s.move.x;
      s.hed.y += s.move.y;
      // --dead--
      var b1 = (s.hed.x<self.w);
      var b2 = (s.hed.y<self.h);
      var b3 = (s.hed.x>=0);
      var b4 = (s.hed.y>=0);
      var b5 = (s.body.filter((v)=>{return (s.hed.x==v.x)&&(s.hed.y==v.y);}).length==0);
      if(!(b1&&b2&&b3&&b4&&b5)){
        return false;
      }
      // --eat--
      if(self.map[s.hed.y][s.hed.x]=="food"){
        if(s.body.length==0){
          s.body.push(JSON.parse(JSON.stringify(s.hed)));
        }else{
          s.body.push(JSON.parse(JSON.stringify(s.body[s.body.length-1])));
        }
        s.point+=1/self.tm;
      }
      self.map[s.hed.y][s.hed.x]=0;
      // --food--
      var foody=Math.floor(Math.random()*self.h);
      var foodx=Math.floor(Math.random()*self.w);
      var fb1=(s.body.filter((v)=>{return (foodx==v.x)==(foody==v.y);}).length==0);
      var fb2=(!((s.hed.x==foodx)&&(s.hed.y==foody)));
      if(fb1&&fb1){
        self.map[foody][foodx]="food";
      }
      // --view--
      for(i=0;i<self.h;i++){self.view[i].fill(0);}
      self.view[s.hed.y][s.hed.x]=0x0000ff;
      s.body.forEach((v)=>{
        self.view[v.y][v.x]=0x5555ff;
      });
      self.map.forEach((y,yi)=>{
        y.forEach((x,xi)=>{
          if(x=="food"){
            self.view[yi][xi]=0xff00ff;
          }
        });        
      })

      return true;
    }
    /* 初期化処理
    */
    var _init_=()=>{
      self.st=1;
      self.w=40;
      self.h=40;
      self.view=new Array(self.h);
      for(i=0;i<self.h;i++){
        self.view[i] = new Array(self.w);
        self.view[i].fill(0);
      }
      self.map=new Array(self.h);
      for(i=0;i<self.h;i++){
        self.map[i] = new Array(self.w);
        self.map[i].fill(0);
      }
      self.snake={
        hed:{x:Math.floor(self.w/2),y:Math.floor(self.h/2)},
        body:[],
        point:0,
        move:{x:1,y:0}
      }
      self.tm=300;
      document.onkeydown = function (e){
        var k = e.keyCode;
        var s = self.snake;
        console.log(k)
        if(k==37)s.move={x: 0,y:-1};//左
        if(k==38)s.move={x:-1,y: 0};//上
        if(k==39)s.move={x: 0,y: 1};//右
        if(k==40)s.move={x: 1,y: 0};//下
        if(k==32)self.st=2;//game開始
      };
      tm_set();
      console.log("_init_");
    }
    /* マウント前イベント
    */
    self.on("before-mount",(v)=>{
      _init_();
    });
  </script>
</sample>

残課題

  • 死んだら死んだとか出てほしいけど出ない
  • ポイントが後半に食った餌ほど線形に高い雑仕様
  • 死んだら得たポイント見られない
  • 毎更新に餌がおかれるため、後半めっちゃ出る