視聴者のコメントと戦うゲームを作った話

今回ご紹介するのはこちらです。


ツイートの日付からもわかるように、これは2年前につくったものなのですが、
しっかりとブログに書いたことがなかったので、改めて記事にさせて頂きます。

ちなみに、この記事の最後には実際にこのゲームで遊んでみることができますのでお楽しみに!

これにどんな意味があるの

生放送のゲーム実況をする時に、視聴者から頂くコメントはとても重要です。
なにより配信者のモチベーションが上がりますし、
コメントが盛り上がっている放送には人が集まりやすいです。

実際にYouTubeなどのプラットフォーム側でも、
コメントが盛り上がっている放送ほどピックアップされる傾向にある気がします。

そこで放送開始時に視聴者がコメントをしやすくなる仕掛けを作り、
また、ある程度のゲーム性を持たせ、視聴者も参加する楽しみがあることで
放送開始時に視聴者数を一気に上げる目的があります。

これはどんなもの?

まずゲームのタイトル画面です。

これは、このまま放送の待機画面(フタ絵)の代わりとして使うことができます。
中央に「まもなく生放送が始まります」と表示され、背景の街が右から左に流れていきます。
同時に視聴者から届いたコメントが、カラフルな文字で流れていきます。

ゲームを開始すると、左側に僕ら「とかい育ち」の3人のキャラクターが出現します。

キャラクターはジャンプをしながら右に進んでいきます。
流れてくるコメントに当たると、左に追いやられてしまいます。
右上に制限時間が表示されており、この残り時間を逃げ切ったら配信者の勝ち
途中で視聴者のコメントに邪魔され、画面の左端に行ってしまったら視聴者の勝ち

視聴者がコメントをした回数と、活躍した人の名前が表示されます。
(画像はサンプルなので0回と表示されています)
また、コメントの総数と生存時間(とユニークユーザー数)からスコアを算出します。
ユーザーは、高得点かつ活躍した人を目指して、コメントを頑張ってもらいます。

どうやって動いているか

このゲームは全てjavascript+cssで動いています。
はじめて作ったブラウザゲームなので、色々非効率な部分があると思いますがご容赦ください。。

ちなみにTwitterには偉そうにhtml5と書きましたがCanvasは使っておりません。。
<div>タグにbackground-imageでキャラクターや背景などの画像を充てて、jQueryで操作しています。

動く背景はCSS

天井・床・背景は、こんな感じでCSSで動いています。

@keyframes animSky {
   0% { background-position: 0px 100%; }
   100% { background-position: -5500px 100%; }
}

#sky {
    z-index: 1;
	position: absolute;
	top: -100px;
	width: 100%;
	height: 100%;
	background-image: url('../assets/sky.png');
	background-repeat: repeat-x;
	background-position: 0px 100%;
	background-color: #a2d6ca;
	animation: animSky 140s linear infinite;
}

メインループ

基本的な構造はメイン処理gameloop()
(60fpsでるように)1000/60ミリ秒でsetIntervalで実行してあげます。

let gameloop = () => {
  // 残り時間の確認
  lastTime = endTime - nowTime();
  if (lastTime) $('#score').text(lastTime);
  else endGame();

  // プレイヤーの基本移動距離
  velocity += gravity;
  positionY += velocity;
  positionX += acceleration;

  // プレイヤーの座標検出
  let playerTop = positionY - playerHeightSize;
  let playerBottom = positionY + playerHeightSize;
  let playerLeft = positionX - playerWidthtSize;
  let playerRight = positionX + playerWidthtSize;

  // ゲームオーバーの判定
  if (playerRight + playerWidthtSize <= 0) {
    endGame();
    return;
  }

  // コメントとの衝突判定
  if (cmnt.length) {
    for (let i in cmnt) {
      let cmntWidth = parseInt(cmnt[i].css('width'));
      let cmntHeight = parseInt(cmnt[i].css('height'));
      let cmntTop = cmnt[i].offset().top;
      let cmntBottom = cmntTop + cmntHeight;
      let cmntLeft = cmnt[i].offset().left;
      let cmntRight = cmntLeft + cmntWidth;

      // プレイヤーの位置がコメントの範囲内(上下)
      if (playerTop <= cmntBottom && playerBottom >= cmntTop) {

        // プレイヤーの右部がコメントに衝突する判定(押し返される)
        if (playerRight >= cmntLeft && playerLeft <= cmntRight) {
          positionX -= 5;
          positionY += velocity / 2; // 上下に少し加速(ハマり防止)
        }
      }

      // プレイヤーの位置がコメントの範囲内(左右)※上の判定と差異をつける目的で+5(ハマり防止)
      if (playerRight >= cmntLeft + 5 && playerLeft <= cmntRight) {

        // プレイヤー下部がコメントに衝突する判定(床と同じ)
        if (playerBottom >= cmntTop + 10 && playerBottom <= cmntBottom) {
          setUpdatePlayer(positionX, cmntTop - playerHeightSize);
          positionY -= velocity * 1.5; // 押し出された時の上下加速の辻褄合わせで1.5倍
          velocity -= gravity;
          //return;

          // プレイヤー上部がコメントに衝突する判定(天井と同じ)
        } else if (playerTop <= cmntBottom && playerTop >= cmntTop) {
          setUpdatePlayer(positionX, cmntBottom + playerHeightSize);
          velocity = 1;
          //return;
        }
      }
    }
  }

  // 床との衝突判定
  if (playerBottom >= landY) {
    setUpdatePlayer(positionX, landY - playerHeightSize);
    positionY -= velocity;
    velocity -= gravity;
    return;
  }

  // 天井との衝突判定
  if (playerTop <= ceilingY) {
    setUpdatePlayer(positionX, ceilingY + playerHeightSize);
    velocity = 1;
    return;
  }

  // プレイヤーを移動
  setUpdatePlayer(positionX, positionY);
}

この時の処理のほとんどは当たり判定衝突判定)が占めています。
毎フレーム、キャラクター各コメントの座標を調べて、
コメント左側・上側の面キャラクターに触れていないか調べ、
触れていたらキャラクターを押してあげます。

この衝突判定つくるのにめちゃくちゃ苦労しました。。
そしてこれで合ってるのかいまだにわかりませんw

ゲームオーバー

残り時間が0になるか、キャラクターが画面の左端に到達したらendGame()を呼びます。

let endGame = () => {
    // 2: ゲームオーバー
    gamemode = 2;
    // 3: ゲーム再開の待機状態
    setTimeout(()=>{ gamemode = 3 },1000)

    // サウンド処理
    soundBgm.stop();
    soundJump.stop();
    soundOver.play();

    // ゲーム中の画面を非表示にして、リザルト画面を表示する
    $('#result').css('display','block');
    $('#resultText').css('display','block');
    $('#uniqueUser').css('display','block');
    $('.resultCount').text(allCmnt.length);
    $('.resultPoint').text(calcPoint());
    $('.resultName').text( cmnt[0].attr('username') );
    $('#nicoArea').css('display','none');
    $('.chara').css('display','none');
    $('#score').css('display','none');
    $('#sky').css('animation','none');
    $('#land').css('animation','none');
    $('#ceiling').css('animation','none');

    // ループは全てクリアする
    if(gameLoop) clearInterval(gameLoop);
    if(xmlLoop) clearInterval(xmlLoop);
    if(starLoop) clearInterval(starLoop);
    if(debugLoop) clearInterval(debugLoop);
}

コメントの取得に関する部分

コメントの取得は、外部のコメントビュアーソフトが使えるように統一規格に対応させました。
comment.xmlというファイルにXML形式に出力されるものです。

let getCommentXml = () => {
  let t = new Date().getTime();
  $.ajax({
    url: fileName + '?' + t,
    type: 'GET',
    dataType: 'xml',
    timeout: 500,
    error: function () {
      console.log('コメントジェネレーターのロードに失敗しました');
    },
    success: function (xml) {
      let xmlIndex;
      let xmlLength = $(xml).find('comment').length;
      if (latestXmlCount) { // 初回起動ではない
        if (latestXmlCount != xmlLength) { // コメント数に変動があった
          var newXmlCount = xmlLength - latestXmlCount;
          if (xmlLength < latestXmlCount) {
            latestXmlCount = xmlLength - 1; // コメント数がマイナスだった場合は最新1つのみ取得する
          } else {
            for (let i = latestXmlCount; i < xmlLength; i++) { // 更新のあった回数だけループ
              let newComment = $(xml).find('comment').eq(i);
              let owner = newComment.attr('owner');
              let handleName = newComment.attr('handle');
              if (!handleName) handleName = "名無しさん";
              let textNo = newComment.attr('no');
              tempTime = parseInt(newComment.attr('time'));
              let site = newComment.attr('service');
              let text = newComment.text();
              if (site != 'hidden' && site != 'log' && site != 'geneLog') {
                nicoGenerator(handleName, text, site);
              }
            }
            latestXmlCount = xmlLength;
          }
        }
      } else { // 初回起動時
        latestXmlCount = xmlLength;
      }
    }
  });
}

ただ、今はこの規格あまり使われていないかもしれませんので、
直接PF側のコメント用のAPIを叩きに行ってもいいかもしれません。
例えばYouTubeLiveなら、こんな感じです。

let getComments = (chatId) => {
  let apiUrl = 'https://www.googleapis.com/youtube/v3/liveChat/messages';
  let pageTokenParam = '';
  if (pageToken) pageTokenParam = '&pageToken=' + pageToken;

  $.ajax({
    url: apiUrl + '?liveChatId=' + chatId + '&part=snippet,authorDetails&maxResults=200' + pageTokenParam + '&key=' + YT_API_KEY,
    type: 'GET',
    dataType: 'json',
    timeout: 1000,
    error: function () {
      console.log('YouTube Live Streaming API LiveChatMessages: list に接続できませんでした。');
    },
    success: function (json) {
      pageToken = json.nextPageToken;
      if (!pageTokenParam) return;
      if (json.items.length) {
        for (var i = 0; i < json.items.length; i++) {

          let time = new Date(json.items[i].snippet.publishedAt).getTime();
          if (lastRead < time) {
            lastRead = time;

            let chatType = json.items[i].snippet.type;
            let chatText = json.items[i].snippet.displayMessage;
            let chatName = json.items[i].authorDetails.displayName;

            if (chatType == 'superChatEvent') {
              let superChatAmount = json.items[i].snippet.superChatDetails.amountDisplayString;
              let superChatTier = json.items[i].snippet.superChatDetails.tier;
              console.log(chatName + ' からSuperChat(' + superChatAmount + ') Tier:' + superChatTier + ' が届きました');

            } else console.log(chatName + ': ' + chatText);
          }
        }
      }
    }
  });
}

このコードは数年前に書いたものなので今も動くかはわかりません。。。

そんなこんなで..完成!

こうして視聴者参加型ゲーム(コードネームTOK-FLY)は完成しました!
とてもいいアイデアだと思ったのですが、実際は想定していたほどコメント数が稼げず、
gdgdになってしまったので、使うのをやめてしまいました。

その後、実はとある配信者から依頼を受け、このTOK-FLYは改良を加えて巣立っていったのです。
(そして少しの間、中国のサイトで活躍していた)

もし配信者さんで、こういうギミックを(お金がかかってもいいから)作りたい!という方がいたら、
気軽にお声掛けください(⊃∪`*)

おまけ

お待たせしました!是非遊んでみて下さい(コメントはダミーのものが流れます)
スマホの方や、うまく表示されない方はここをクリックして下さい。
(それでもうまく表示されない可能性があります)
クリックでゲーム開始&ジャンプします。

投稿者

じん
「とかい育ち」という3人組でゲーム実況やってます!
生放送の画面作りにこだわりだして、そのことをブログに書かせてもらってます。
プログラミングは独学で素人レベルですが、動けばいいの精神で色々作ってます!

コメントを書く

*
*
* (公開されません)