Help us understand the problem. What is going on with this article?

Riot.jsで蛇のゲーム

More than 1 year has passed since last update.

背景

個人的に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>

残課題

  • 死んだら死んだとか出てほしいけど出ない
  • ポイントが後半に食った餌ほど線形に高い雑仕様
  • 死んだら得たポイント見られない
  • 毎更新に餌がおかれるため、後半めっちゃ出る
hashito
わたしです。 主に個人的なメモです。
https://hashito.biz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away