LoginSignup
4
5

More than 3 years have passed since last update.

p5.jsとDjangoでインタラクティブシミュレーション(4) p5.jsとDjangoの間のajaxでのデータのやり取り

Last updated at Posted at 2019-04-11

はじめに

筆者のラボでは,人を含むシステムの挙動を分析するために,またそうしたシステムの中に置かれた人の挙動を分析するために,ユーザが途中で介入できる形式のコンピュータシミュレーション(インタラクティブシミュレーション)をウェブ上のゲームとして開発し,よく利用している.

このドキュメントは,そうしたインタラクティブシミュレーションのアプリケーションをp5.jsとDjangoを用いて開発するための基礎を身につけてもらうことを狙いとしたもので,あまり一般的なニーズはないかもしれないが,もし多少でもどなたかの参考になれば幸いだ.

今回は全4回中の最終回で,全体のコードはまとめてGitHubに置いた.

p5.jsとDjangoの間のajaxでのデータのやり取り

前回まででインタラクティブシミュレーションのプログラムをp5.jsで作成しwebに公開することができるようになった.実は,それだけが目的ならあえてDjangoを後ろに置く必要はない.

では,なぜp5.jsをDjangoと組み合わせるのかというと,シミュレーションのログをサーバ側に引き取ってデータベースに格納したり,サーバ側で適切に設定したパラメータをシミュレーションに送り込んだり,といったことを実現するためである.

インタラクティブシミュレーションを研究の手段として用いるためには,これらの機能は欠かすことができない.今回は,そうした機能の基礎として,p5.jsのシミュレーションとDjangoの間のajaxでのデータのやり取りの基本を押さえよう.

Django側の準備

最初にDjangoにログデータを格納するためのデータ構造のクラスを追加しておこう.具体的には,sim/models.pyに下記の内容を書き込む.

from django.db import models

class Game(models.Model):
    score = models.IntegerField(default=0)
    def __str__(self):
        return u"game_{}".format(self.pk)

class State(models.Model):
    game = models.ForeignKey(Game, on_delete=models.CASCADE)
    time = models.FloatField()
    vol = models.IntegerField()
    ordered = models.IntegerField()
    outs = models.IntegerField()
    holding_cost = models.FloatField()
    ordering_cost = models.IntegerField()
    revenue = models.IntegerField()
    def __str__(self):
        return u"state_{}_{}".format(self.game.pk, self.pk)

Gameクラスは,インタラクティブシミュレーションの1試行に対応しており,その試行のスコアの値を保持するようになっている.Stateクラスは,シミュレーションモデル(my_model)のstateLogの各要素に対応するデータを保持するためのデータ構造(ただし,簡単のため,orderedはリスト全体ではなくその先頭の値のみを整数で保持するようにしている)であり,多対1の関係でGameクラスと関係づけられている.

これらのクラスはadminサイトにも登録しておくと便利である.そのためには,sim/admin.pyに次の内容を書き込んでおけばよい.

from django.contrib import admin
from . import models

class StateInline(admin.TabularInline):
    model = models.State
    extra = 0

class GameAdmin(admin.ModelAdmin):
    inlines = [StateInline]

admin.site.register(models.Game, GameAdmin)

続いて,ajaxでのデータ通信を処理するためのview関数を用意する.具体的には,sim/views.pyの中に次の2つの関数を定義する.

import json
from django.shortcuts import render
from django.http import JsonResponse, HttpResponseServerError
from . import models

def get_games(request):
    games = models.Game.objects.all().order_by("-score").values()
    gamelist = list(games)
    return JsonResponse(gamelist, safe=False)

def post_logs(request):
    if request.method == 'POST' and request.body:
        json_dict = json.loads(request.body)
        score = int(json_dict['score'])
        game = models.Game.objects.create(score=score);
        for log in json_dict['logs'].values():
            models.State.objects.create(
                game=game,
                time=float(log['time']),
                vol=int(log['vol']),
                ordered=int(log['ordered']),
                outs=int(log['outs']),
                holding_cost=float(log['hc']),
                ordering_cost=int(log['oc']),
                revenue=int(log['rv']),
            );
        return JsonResponse(json_dict)
    else:
        return HttpResponseServerError()

get_games()関数は,データベースから取得した(Gameオブジェクトの)クエリセットをリストにして,さらにそれをJsonResponseに変換して返していることがみてとれる.これがGETでのajaxリクエストに応える典型的なパターンである.

一方,post_logs()関数の方は,クライアント側からPOSTで受け取ったログデータをデータベースに格納する処理を担当している.

送られてくるのはJSON形式のデータ(request.body)であり,それをまずjson.loads()pythonの辞書に変換する.この段階では,まだ辞書の値は文字列になっているので,続いて,必要に応じて,int()float()で型変換を施しているのがわかる.

また,GameとStateのどちらにもcreate()メソッドを用いているが,このメソッドは,新しいオブジェクトの生成とセーブを同時に行う(ため,後でsave()メソッドを呼ぶ必要はない).これは,POSTでのajaxリクエストに応える典型的なパターンである.

これらのview関数をテンプレート側から利用できるようにするためにurlルーティングも追加しておく.sim/urls.pyを次のように変更しておこう.

from django.urls import path
from django.views.generic import TemplateView
from . import views

app_name = 'sim'

urlpatterns = [
    path('', TemplateView.as_view(template_name='sim/index.html'), name='index'),
    path('get/', views.get_games, name='get_games'),
    path('post/', views.post_logs, name='post_logs'),
]

これでDjango側の準備は整った(ただし,makemigrationsとmigrateを忘れないこと).続いて,p5.jsのコードの方にDjangoとのやり取りに関する機能を追加していく.

p5.js側の拡張

まず,シミュレーション終了時にshow_results()で表示する画面に,今回のスコアの詳細だけでなく,これまでのベストスコアや,今回の順位についての情報を含めるようにする.

そのために,update()メソッドの

if(e.type == "over") {
  my_model.show_results();
  noLoop();

の箇所を次のように書き換える.

if(e.type == "over") {
  loadJSON("/sim/get/", my_model.close_game.bind(this));
  noLoop();

loadJSON()は,p5.jsに用意されているJSONデータをajaxで取得してくるための関数である.第1引数がurlを指定しており,上で指定したurlルーティングによって,これがget_games()のview関数に紐付けられることがわかる.

第2引数はいわゆるコールバック関数で,JSONデータを取得し終わったらそのデータを引数にしてこの関数が呼ばれることになる.ここでは,コールバック関数として,my_modelclose_game()メソッドに.bind(this)をつけて利用している.

close_game()メソッドは次のように定義した.

Model.prototype.close_game = function(games) {
  var scores = [];
  for(var game of games) {
    scores.push(parseInt(game.score));
  }
  var my_score = this.calc_score();
  var best_score = my_score;
  var my_rank = 1;
  if(scores.length > 0) {
    best_score = max(best_score, scores[0]);
    for(var score of scores) {
      if(score > my_score) {
        my_rank ++;
      } else {
        break;
      }
    }
  }
  this.show_results(my_score, best_score, my_rank, scores.length +1);
  this.save_log();
}

view関数get_games()から返されてくるJSONデータから過去のスコアのリスト(scores)を作成し,それと今回のスコア(my_score)を比較しながら,今回の順位(my_rank)やこれまでのベストスコア(best_score)を算出していることがわかる.

最後にそれらのデータを引数として,show_results()メソッドとsave_log()メソッドを呼んでいる.ここでは,show_results()の詳細は省略するが,今回のスコアに加えて,順位やベストスコアなども表示するように少し拡張を加えた.

また,ゲームスコアの計算はcalc_score()というメソッドにまとめている.

Model.prototype.calc_score = function() {
  return floor(
    this.state.rv -this.state.hc -this.state.oc -this.state.outs *this.par.SOP
  );
}

save_log()メソッドについて紹介する前に,.bind(this)についてみておこう.これはjavaScriptのthisの挙動と関係がある.

仮に,close_game()my_modelのメソッドとして単純に,

my_model.close_game();

のように呼び出した場合は,直感どおり,thismy_modelオブジェクトを指す.

ところが,my_model.close_gameがコールバック関数として別の関数などに渡された場合は,close_game()の関数としての定義だけが引数として送り込まれ,それが後で利用される.その際,引数として送り込まれた関数とmy_modelオブジェクトとの対応関係は見えなくなり,thismy_modelを指さなくなってしまう(グローバルオブジェクトを指すようになる).

そこで,コールバック関数(だけではないが)の呼び出し時に,強制的にthisを指定できるようにするのが.bind()の機能である.上では,update()メソッドが動いている際のthis(すなわちmy_model)を.bind()の引数に与えて,my_model.close_gameが呼び出されたときのthismy_modelを指すように仕組んでいるわけである.

続いて,save_log()メソッドについてみていく.これは,ログデータをajaxでDjangoに送り込むためのメソッドである.

このメソッドでは,結果としてデータベースに変更を加えることになるため,POSTを利用する.p5.jsにはhttpPost()というPOSTのための関数も用意されているが,Djangoのcsrf対策のために必要なヘッダ情報を付与することができないので,Djangoとの通信に利用するのは難しい.

そこで,JavaScriptクッキーライブラリaxiosを用いることにする.まず,これらを利用できるようにするために,index.htmlの中に次の2行を加えておく.

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>

そして,save_log()メソッドを次のように定義する.

Model.prototype.save_log = function() {
  var csrftoken = Cookies.get('csrftoken');
  var headers = {'X-CSRFToken': csrftoken};
  var data = {
    "score": this.calc_score(),
    "logs": {}
  };
  for(var i = 0; i < this.stateLog.length; i++) {
    var log = Object.assign({}, this.stateLog[i]);
    log.ordered = log.ordered.length ? log.ordered[0] : 0;
    data.logs[String(i)] = log;
  }
  console.log(data);
  axios.post("/sim/post/", data, {headers: headers})
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
}

2行目と3行目でcsrf対策のためのヘッダ情報(headers)を作成し,それをaxios.post()を呼ぶ際にデータと一緒に渡していることがわかる.データの中身はスコアとstateLogであり,上で作成したview関数post_logs()に対応した形式でJSONデータを送り込んでいる.

ここまでで,シミュレーションが終了すると,それに対応したログデータがデータベースに格納されるようになった.

シミュレーションの再実行ボタン

もう一度シミュレーションを最初から開始したくなった場合は,このままでもリロードすればよいが,最後に,そのためのボタンを追加してみよう.setup()の中に,ボタンを1つ付け加える.

var reset_btn = createButton("Play Again");
reset_btn.parent("buttons");
reset_btn.mousePressed(reset_sim);

そして,このボタンに対応するコールバック関数reset_sim()を次のように定義する.

function reset_sim() {
  if(frameCount >= 900) {
    my_model = new Model();
    frameCount = 0;
    loop();
  }
}

これで,このボタンを押せば再度シミュレーションが最初から開始されるようになった.

おわりに

以上で最終回も終了.

ここまで読んでくださった方に感謝. m(__)m

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5