読者です 読者をやめる 読者になる 読者になる

日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

ASP.NET アプリを IIS に配置する(ただし一度パッケージングしてからのやり方)

ASP.NET IIS

はじめに

2017/01/23 加筆修正

自分でおさらい的に見返してて足りない部分があったのを追加した。

  • Web 公開ウィザードで Web Deploy Package 作成時複数ファイル出来上がる事などを追加
  • インポートをどこでやるのかを書くのを忘れていたので追加

やりたい事の要点

  • 前回作った ASP.NET Web API を使う最低限の構成の ASP.NET アプリをローカルで一度パッケージングしてから(ここ大事) そのパッケージを使って IIS に配置・ホスティングしたい

経緯的なもの

さて、前回の記事ASP.NET Web API の最低限の構成となるものが何なのかは理解できてローカルではあるが動作確認も取れた。

次はこれをリモートの IIS が動く Windows サーバに配置してホスティングするにはどうすれば良いのか次に調べだした。

ただ、アプリの配置には条件があり、一旦インストーラとか zip とかパッケージ的なものをローカルで一度固めて それをリモートの IIS が動くサーバに転送して配置する必要がある。

Visual Studio から直接リモートの IIS に接続してアプリを配置する方法はググるとすぐに見つかる。 Visual Studio Web プロジェクトの公開ウィザードっぽいもの*1を使った方法である。 ただこの最初に見つかる方法は Visual Studio から直接サーバに繋いで配置するやり方でこの方法は今回の自分の状況では使えない。

自分が望む条件ももう少し調べると同じウィザード画面で Web Deploy Package を選択すればできそうなのだが、 実際の IIS への配置までの手順を通してやったものが何故か中々見つからず調べるのに少し骨が折れた。

まあ一応 MSDN でそれっぽいのが見つかったが

https://msdn.microsoft.com/ja-jp/library/dd483479(v=vs.100).aspx

クドいし自分が知りたい事以外の情報が多すぎる・・・。

多分今回の目的に一番合うのは英語だが以下の記事の方がよりシンプルだと思う。

https://msdn.microsoft.com/en-us/library/dd465323(v=vs.110).aspx

今回は本当にトライ&エラーの果てに自分のこの記事でまとめた方法にたどり着いた感じで、 (と言っても別にトリッキーな方法を使った訳でもなく単に必要な情報をピックアップするのに手間取っただけ) 作業メモ的な側面がいつもより強いかも。

手順概要

  1. Visual Studio 2015 の Web 公開ウィザード(?) で Web Deploy Package を作成する
  2. アプリ配置前 IIS 設定(必要なら)
  3. IIS マネージャーから作成した Web Deploy Package をインポートする

Visual Studio 2015 の Web 公開ウィザード(?) で Web Deploy Pakcage を作成する

  • Visual Studio 2015(以下 VS2015) で動作確認済の ASP.NET Web アプリプロジェクトを開く
  • ソリューションエクスプローラでプロジェクトの所を右クリック -> 公開(B)

  • Web 公開ウィザード(?) で以下のように進める

    • プロファイル
      • カスタム選択
        • プロファイル名は適当に決める。残りの設定が終われば以後このカスタムプロファイルを選択。
    • 接続

      • Publish Method は「Web Deploy Package」を選択
      • Package Location は適当に選ぶ。複数のファイルができるのでソリューションフォルダ直下に out とか作ってそこ指定した方がいいかも。
      • IIS でのサイト名を決める。これも面倒なんでプロジェクト名と同じにした。
    • 設定

      • Configuration は Relese で。サーバ上でデバッグしたい場合は Debug の方が良い気がするが未確認。
      • File Publish Option で Precompile during publishing に何となくチェック
        • これも別にチェック入れなくても支障はない気がする。挙動が速くなりそうな気がしたので。

ここまで設定して「発行」を押すと、指定した Package Location の中に結構色々なファイルが生成される。 ただ、この後のインポートで必要なのは生成された zip ファイルのみ。 一応念の為サーバへ全部を持っていこうと全体を zip してみたら 約4MBだった。結構サイズでかいんだな。

アプリ配置前 IIS 設定(必要なら)

配置サーバ環境

  • Windows Server 2008(英語版)
  • IIS 7.5
    • IIS .NET 4.0 モジュールはインストール済っぽい。後述の手順で選べたからというだけで、ちゃんとした確認方法は未確認。

手順などで単語が英語多めになるけどご勘弁。

Default Application Pool の .NET Version 変更

IIS Manager を開き、Default Web Site の Application Pools で DefaultAppPool の .NET Framework Version を v4.0.30319 に変更する。 または新たに Web Site を追加して DefaultAppPool を v4.0.30319 にする。

これをやらないと WebAPI に対応する URL にいくらアクセスしても HTTT 404 エラーが返ってくるので大事。

IIS マネージャーから作成した Web Deploy Package をインポートする

  1. Default Web Site 右クリック -> Deploy -> Import Application...
  2. 何やらインポートウィザードらしきものが出て、パッケージファイルを選択しろと出るので、Web 発行ウィザードで生成された zip を選択
  3. インポートするファイル選択など出るが、特に何もせず次へと押していく

動作確認は URL を前回の Localhost から今回配置したリモート IIS への URL に変えるだけなので割愛。

グチ

全く・・・この情報にたどり付くのにほぼ丸一日かかった。 これかもしれないと気づいたリンクは こちらで、 そのリンク先は探している途中で既に通っていた(読んでいるとは言ってない) MSDN のチュートリアル記事 の中に書いている事に後で気づきさらに疲れた。。。

網羅的に全部書くのはそれはそれでいいんだけどもう少し分かり易いサマリーと言うか項目まとめも付けてくれんかねえ。

*1:公式名称は良く分からない

Java, Clojure くらいしか知らない自分が ASP.NET 上で動く Web サービスをイチから作る

.NET ASP.NET Web API

はじめに

前回の記事 で NuGet の使い方から始まったのはこいつを使いたかったから。

.NET上で動き、IIS 上でホスティングする Web サービスを作る必要性が出てきて、 現状.NET 界隈を調べた感じ ASP.NET Web API.aspx) 使って作るのが 一番良さそうに思った。

Visual Studio 2015 だとプロジェクトテンプレートがあり、最初それを参考にすればいいかなと思ってたのだが、 見てるとどうも結構 ASP.NET Web API とは直接関係ないものも結構入っていて C#/ASP.NET 未経験の自分は混乱した。

時間はかかるかもしれないが、こりゃまずイチから作った方が良さそうな気がしたので 改めてその方向でググると日本語でバッチリの記事が見つかった。

読んでみて雰囲気は分かるが、細かい部分でなぜそうなるのかが分からない部分がかなり多くつまづいた。 2,3程度ならその場でちょっと調べて済ませようと思ったが、 かなり調べる事になったので、作業の流れには添いつつも自分の知識が不足してる面を補足的にメモする事にした。

なお、上記記事は動作確認までしか試してない。 これは単に自分がまず知っておくべきなのはそこまでかなと思っただけ。

実行環境

手順概要

ASP.NET Web API を使った最低限のコードを動作させるには以下の手順を踏む必要がある。

# 手順 元記事該当セクション
1 ASP.NET 空 Web アプリプロジェクトを作る プロジェクトの作成
2 外部ライブラリの依存性の解決 必要な参照の追加
3 WebAPI 初期設定を行うクラスファイルを用意する Global.asaxの追加
4 Global.asax に初期設定コードを記述する Global.asaxの追加
5 実際の API となるコントローラクラスの追加 動作確認
6 動作確認 動作確認

自分の理解のため、やや冗長ではあるが元記事より項目を細分化した。 この記事では以降、手順の方でタイトルを書いていく。

ASP.NET 空 Web アプリプロジェクトを作る

Visual Studio 2015 のメニューから ファイル -> 新規作成 -> プロジェクト で ASP.NET Web アプリケーション -> Empty で 空のプロジェクトが作成される。 この時点でそこそこいくつかフォルダやファイルが出来ているが、今回は必要そうなもの以外は無視して 何もしない事にする。

外部ライブラリの依存性の解決

他言語環境でのパッケージマネージャでは依存性の解決とか呼んだりするアレ。

ツール -> NuGet パッケージマネージャー -> ソリューションの NuGet パッケージの管理 から出る画面左上の検索ボックスにMicrosoft.AspNet.WebApiを検索すれば出てくる完全一致のもの (多分検索結果の一番上に出ると思う) にチェックしてインストールすれば良い。

前回の記事にあったように、一度ダウンロードしたものはキャッシュとして指定場所に保存されている。

WebAPI 初期設定を行うクラスファイルを用意する

ASP.NET WebAPI でこの役割を担うのがグローバルアプリケーションクラスであり、 ファイル名はGlobal.asaxと決まっている模様。

Global.asax 自体は ASP.NET WebAPI 専用と言うわけでもなく、ASP.NET 登場当初からあった ASP.NET Web アプリのグローバル設定を持たせるためのファイルらしい。 (@ITの参考記事)

Visual Studio から追加した場合、実際に WebAPI 初期化処理を書く場所となるメソッドである Application_Start だけでなく 他にも色々と追加されるが、今回は不要なので Application_Start メソッドのみ残し他は削除した。

Application_Start が呼び出されるタイミングなどは以下の記事が参考になった。 - ASP.NETのライフサイクルの仕組み - このサイトによると ASP.NET Web アプリが呼び出される最初の一回だけらしい。MSDN でもどっかで見たような気がするけどブックマークしとくの忘れた。

Global.asax に初期設定コードを記述する

元記事では必要ファイルの追加とコードの内容だけ書いてかなり短く済ませており、C#/ASP.NET 経験者には十分なのかもしれないが、 自分はかなり分からない部分がありここで色々調べた。

疑問1: App_Start フォルダ作成は必須なのか?

Visual Studio デフォルトで用意されている ASP.NET Web API プロジェクトテンプレート作成する方法では確かに作られているが stackoverflow での同様の質問と回答 を見る限りでは、別に ASP.NET アプリを動作させるための必須フォルダと言うわけでもなさそう。 回答に書かれているリンク先ブログにもっと詳しく書かれてそうな記事があったが、 自分はひとまず必須かそうでないかだけを知りたかっただけなので今回は読んでいない。

疑問2: WebApiConfig クラスとその Register メソッドは必須なのか?

英語だが、MS公式サイトにある一文が最も分かり易かった。

In an ASP.NET application, configure Web API by calling GlobalConfiguration.Configure in the Application_Start method. (引用元)

疑問に対する答えとしては別に必須ではない、となる。

元記事でのRegisterは結局は初期設定を持つ GlobalConfiguration.Configurationプロパティ.aspx) を引数に渡して書き換えているだけで、名前は極端な話 foo.bar でも動作的には問題なさそう。 *1

対して MS 公式にある方はラムダ式を使ってもっと直接的に書き換えているので、自分の練習も兼ねて今回はラムダ式を使った方法で書き直した。

動作確認

元記事にあるように Visual Studio で Web API Controller クラスを追加すると、 動作するための最低限のテンプレートも入っていて、動作確認だけであれば全くいじる必要はない。

Visual Studio のプロジェクトを実行すると localhost でセルフホスティングする Web サーバが立ち上がってブラウザがそこの URL を開く模様。 ただ、アクセスするURL http://localhost:<port>/ は HTTP 403 forbidden エラーが出る。 これで動作確認失敗と言うわけではなく、登録した ValuesController クラスにアクセスする URL ではないと言うだけ。

Fiddlerを使って http://locahost:<port>/api/values の URL に対して HTTP GET リクエストを出すと、結果がちゃんと返ってきた。 面白いのは Chrome に同じ URL を入れると XML で返ってきて、 Fiddler で Content-Type: application/json ヘッダを付ける (元の記事のやり方)で返すと JSON 形式で返ってきている所。

ASP.NET WebAPI は色んなメッセージング形式に対応しているとだけ聞いていて今回調べていたが ようやく少し実感できたかもしれない。

ちなみに元の記事で使っていた Fiddler は自分も興味があったから使ってみただけで 今回の結果を見たいだけであればブラウザアクセスするだけで間に合う。

調べた時に気にはなったが試してないもの

*1:Java のようにクラス名は大文字からという規則はあるかもしれないがそれは言語仕様レベルの話でまた別

leiningen しか使った事のないやつによる NuGet の使い方メモ

.NET NuGet

はじめに

最近は仕事上の理由からちょっとずつ .NET 関係を調べております。

今日は NuGet のお話。 NuGet は .NET 開発におけるパッケージマネージャー。 JavaMaven に近いポジション?Maven はほとんど使った事ないのでイメージだけで言ってます。

まともに使ってきたものは leiningen しかないのでその中でよく使ってきた機能が NuGet ではどうすればできるのかの観点だけでメモ。パッケージマネージャーを使うと何が嬉しいのかという話はこの記事ではしない。 その辺のメリットはググればいくらでも出てくる記事に任せる。

NuGet に関する網羅的な情報は公式ドキュメントの方にある。

インストール

参考

VisualStudio 拡張版と CLI

NuGet には VisualStudio 2015 上で動作する Visual Studio 拡張版と 単体で動作するコマンドライン(CLI)版の二つが存在する。

NuGet の全機能を使えるのは CLI 版のみらしい。プロジェクト毎に依存ライブラリを解決しつつイ ンストールしてくれるだけで良いのならVisual Studio 拡張版で良さそう。

CLI 版のインストール

ここから Windows x86 Commandline の latest exe をダウンロードし、 PATH の通った所にコピーするのみ。

以降は NuGet CLI についてのみのメモ。 *1

Visual Studio 拡張版のアップデート

Visual Studio 2015 には既に同梱されているとの事だが、今回使おうと思っている CLI 版と バージョンは合わせておいた方が良いかと思いこちらもアップデート。

公式トップにデカデカとある Install NuGet をクリックしてダウンロードされた Visual Studio 拡張ファイル(vsix)をダブルクリックしてインストールするだけ。

アップデート前のバージョンが何だったかは控えておらず失念。 *2

バージョン確認

CLI

> nuget
NuGet Version: 3.5.0.1938
usage: NuGet <command> [args] [options]
Type 'NuGet help <command>' for help on a specific command.

(以降各サブコマンドの簡易説明があるが省略)

Visual Studio 拡張版

メニューの - ツール->拡張機能と更新プログラム から開かれるダイアログで NuGet で検索して出てくる「NuGet パッケージマネージャー」を選択すると、 右にバージョン情報など出るのでそこで確認。 *3

今回自分が入れたのは 3.5.0.1996 だった。一番下の表記が CLI 版と微妙に違うけど大丈夫なんかな?

設定一覧

個別設定なら nuget config で行けそうだが、一覧をコマンドラインで見る方法は見つからず。 設定ファイルとその適用順に関しては公式のこちら に詳しく書いてある。

おおまかなイメージ的にはグローバルとプロジェクト単位それぞれで設定を持てるみたい。 設定が被っている場合の適用順はプロジェクト単位から優先して適用されるっぽい。

VisualStudio で表示・変更できる設定は?

%APPDATA%\NuGet\NuGet.Config にある。Visual Studio 2015 の GUI で変更した設定はこのファイルに反映される。 なお、これは CLI 版でもグローバル設定と言う扱い。

ちなみに Visual Studio 2017 及び NuGet 4.0 以降では Visual Studio のバージョン別に全ユーザに適用される設定も 持てるようになったとか。

パッケージの生成的なものはあるか

少しドキュメント探してみた感じだと lein uberjar 的なコマンドなさそうだなあ。

いや、nuget pack と言う一見それっぽいのはあるんだが、これで生成可能なパッケージは lein uberjar がカバーする範囲とは少し異なり、 外部ライブラリとか Visual Studio 拡張向けのパッケージ生成は可能だが、 Java の jar のような単体アプリとしても動作するようなパッケージを作る場合が見当たらなかった。

ローカル PC にパッケージリポジトリを持たせる

leiningen での依存関係の解決でのデフォルト動作のように、ライブラリを探しに行く時に ローカル PC に保持したリポジトリを探し、なければそこにダウンロードしてインストールする動作を NuGet でもやりたい。

と思って調べてみると、どうも NuGet 2.8 以降ではデフォルトでそういう動作になってるらしい

NuGet ではこういうのを local cache と呼ぶようで、CLI 版だと nuget locals all -list で確認できる。

上記コマンドで出てくるパスの中を実際に確認してみると確かにあった。 自分は NuGet パッケージは Visual Studio 2015 拡張版 NuGet 経由でしか入れてないので恐らく同じ挙動をしていると思われる。

ちなみに特に何も設定しないのにこの項目上げたのはググった時に最初に出てきた情報がプロジェクト毎にしか持てないというのが先に来てたので。 時間が経てばこの辺りの情報が上に来るようになるのを期待し敢えて書いておくことにした。

依存関係の解決

上記の挙動である事が分かったので、これは Visual Studio 上の GUI を使って利用したいライブラリを選んでインストールすれば良い。 CLI でも多分 nuget install を使えば可能だと思うが未検証。

まとめ

特に細かいカスタマイズもせず、外部ライブラリを利用するだけであれば Visual Studio 拡張版で十分だと思った。 公式のダウンロードで Visual Studio 拡張版をトップに持ってくる意図が何となく分かった。

*1:パッケージマネージャーを使った事があるなら、VS 拡張版は特に説明不要な気もする

*2:Visual Studio は再インストールするにはかなりサイズがデカいのであまり再検証する気にもならない

*3:余談だがメニューをたどれば出てくるようなものに対してスクショを貼り付けるのは個人的に記事スペースの無駄だと思う。 全くの PC 初心者向けと言うのならまだしも開発系でもたまにあるのが個人的にあまり好きじゃない。

四角形を動かす、描画する

Clojure play-clj arcade-clj

arcade-clj シリーズ。全体の目次はこちら

元記事での追加機能

かいつまんで言えば自キャラの下ごしらえとして四角を描画してそいつを動かせるようにしましょうと言う内容。

  • 四角形を八方向に動かせるようにする
  • 四角形が動けるのは画面内だけ(=画面外に出ないようにする)
  • 画面をリサイズ可能にし、四角形もリサイズ後の画面を動けるようにする
    • 目次にもあるのだが、リサイズ対応は断念した。
    • 高さがフルに動けて幅が 70% までにしてるらしいが理由が良くわからなかったのでこちらも今回はスルー。 理由が分かった段階で改めて実装する。

動かせるようにする辺りはぼちぼちゲームっぽくなってきたかも。

まずは四角を描画してみる

元記事では SDL API で四角を描画するようにしてて、play-clj で近いものだと shape があるのだが、 残念ながらチュートリアルにあるような位置サイズ変更方法 ができなかったので、texture から同色同サイズの四角画像を読み込んで動かすようにする。 rect.png は desktop/resources 以下に置くこと。

(defn- create-player []
  (assoc (texture "rect.png")
         :x 64 :y 64 :width 32 :height 32))

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    (create-player))

    ;; 以降省略
   )

f:id:shinmuro:20160527205858p:plain

コードは省略してしまったが、main-screen の背景は黒に戻した。

すごくどうでも良い事

スクショ見て気づいたかもしれんけどこの記事書いてる途中で Win10 にアップデートした。

何というかネットで騒がれてるほど大した事なかった。アップデート後いくつか設定戻されてたけどまあ何とかなったし。 けど勝手にアプデスケジュール組まれるのは確かにどうかと思う。自分は押し負けてアップデートしちゃったけど。 まあ無料だし多少不具合が残ってても Windows Update で安定してくるだろうしまあいいかと。

Entity System について

さて、脱線ついでと言う訳でもないんだが、 :on-show に指定している関数で texture タイプではあるものの再度 entity を返すようになったので ここらで一旦 play-clj の Entity System について自分なりの理解を整理しておく。

Entity System については公式のチュートリアルに説明はある。

  • n 個の entity をベクタとして持つ(entities)
  • イベントハンドラ関数の戻り値として entities を前提としている
  • entity になれるものは play_clj.entities.XXXXEntity と言う Clojure の record
    • shape, label, texture など
      • API doc に entity based と書かれているもの
  • entity は record なので HashMap と同じ感覚で key,value として色んな情報を一緒に持たせられる
  • entity として返されないオブジェクトも存在する
    • font, music, sound など
    • この辺りの管理は asset-manager で行う事になるがこれはまた改めて書く

play-clj.repl には entities や screen に保持されてる情報を REPL 上で確認できるユーティリティ関数の es があるので (-main) 実行して画面を出した後で試しに見てみるとどんなものが入ってるのか よりイメージし易いと思う。

四角を動かす

斜め移動の判定

元記事見ると XOR 使って判定してる。なるほどなあ。 *1

(defn- xor?
  [x y]
  (if x
    (if y false true)
    (if y true false)))

(defn- diagonal? []
  (and (xor? (key-pressed? :up) (key-pressed? :down))
       (xor? (key-pressed? :left) (key-pressed? :right))))

論理 XOR は Clojure では標準では無いので 2 項限定でとりあえず自作。 xor? にしたのは true/false しか返さない述語関数の慣習に倣ったもの。

動かすコード

(defn- move-player [e]
  (let [speed 720
        vol (if (diagonal?)
              (/ 1 (Math/sqrt 2.0))
              1)
        moved (* speed vol (.getDeltaTime Gdx/graphics))
        dx (match [(key-pressed? :left) (key-pressed? :right)]
             [true true]   0
             [false false] 0
             [true false] (- moved)
             [false true] moved)
        dy (match [(key-pressed? :up) (key-pressed? :down)]
             [true true]   0
             [false false] 0
             [true false]  moved
             [false true]  (- moved))]
    (-> (update e :x + dx)
        (update :y + dy))))

speed は適当。これに上下左右時 or 斜め時の x/y 移動量と前フレームとの時間差を掛けあわせて移動量を算出。 斜め時の x/y 移動量計算式は元記事からそのまま借用。

x/yの動く方向を含めた移動量が dx, dy になり、その判定に Rust はパターンマッチングを使って いて、これは Rust 学習してた時に結構便利だなーと思っていた。そういえば Clojure にも ライブラリがあった事を思い出し今回初めて使ってみた。 こういう複雑な分岐したい時とかは確かに便利かも。

動かすコードを呼び出すコード

:on-key-down で呼び出せばいいのは想像付くと思うのでその部分だけ載せる。

  :on-key-down
  (fn [screen entities]
    (letfn [(is-key [k] (= (:key screen) (input-keys k)))]
      (cond
        (is-key :space)  (set-screen! arcade-clj-game alt-screen)
        (is-key :escape) (app! :exit)
        (or (some is-key [:up :down :left :right])
            (diagonal?))
        (move-player (first entities))
        
        :else entities)))

is-key はともかくその中の input-keys って何だ?と思うだろう。 これは key-code の関数版を自作したものと思ってもらって良い。 と言っても中身は libGDX の Input.Keys に定義されている static field と key-code で使える keyword を単純に手作業でマッピングした hash-map でしかない。

マクロにしてるのはどうも 実行速度が理由っぽい。 試しに単純に key-codeinput-key を繰り返し実行したものを time マクロで計測してみると 確かに key-code の方が速い。

入力の遅延はゲーム性に直結する話なのでそれはそれで分かるが、ここは自分がコードを書く時の書 きやすさを取った。

実はちょっと実装は異なるが play-clj にも関数版は play-clj.util/key-code* として存在してる事に 作った後で気づいた。入力遅延が気になるレベルで発生したら修正するかも。

画面内のみで動くようにする

この段階で動かしてみると分かるが、画面外へもお構いなしに動かせてしまう。

元記事でも後者は対応していたので合わせて対応しておく。 元記事は親 Rectangle の中に自機となる Rectangle 入れてそれで見るようにしてるけど SDL は知らんけど play-clj は画面サイズ取得できるんでチェックするコードを追加するだけにした。

(defn- constraint-bound-x [{:keys [x width] :as e}]
  (cond
    (neg? x) (assoc e :x 0)
    (> (+ x width) (game :width)) (assoc e :x (- (game :width) width))
    :else e))

(defn- constraint-bound-y [{:keys [y height] :as e}]
  (cond
    (neg? y) (assoc e :y 0)
    (> (+ y height) (game :height)) (assoc e :y (- (game :height) height))
    :else e))

(defn- move-player [e]
  (let [speed 720
        vol (if (diagonal?)
              (/ 1 (Math/sqrt 2.0))
              1)
        moved (* speed vol (.getDeltaTime Gdx/graphics))
        dx (match [(key-pressed? :left) (key-pressed? :right)]
             [true true]   0
             [false false] 0
             [true false] (- moved)
             [false true] moved)
        dy (match [(key-pressed? :up) (key-pressed? :down)]
             [true true]   0
             [false false] 0
             [true false]  moved
             [false true]  (- moved))]
    (-> (update e :x + dx)
        (update :y + dy)
        constraint-bound-x
        constraint-bound-y)))

もうちょい上手い書き方あるかも。

画面のリサイズ対応(→断念)

これは現時点では断念した。

  • play-clj の場合デフォルトでリサイズ可
  • リサイズした場合、texture な四角もサイズが変わってしまう

枠内で動かせるようになっただけで結構満足してしまった。。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.6.0 · shinmuro/arcade-clj · GitHub

*1:項目別のリンクが無かったので元を見てみたい場合はページ内で let diagonal を検索すれば参照可能。

Screen の 切替

Clojure play-clj arcade-clj

arcade-clj シリーズ。全体の目次はこちら

元記事での追加機能

  1. スペースが押されたら背景色を赤⇔緑で切替える

これを元記事では libGDX で言う Screen を 2 つ用意して切替えるようにしている。 機能だけで見れば別に Screen を 2 つ用意しなくても背景色を変更するだけでいいんだが、 せっかくなので play-clj でも Screen を二つ用意して切替えるようにする。

画面切替はどんなゲームでも発生するだろうしね。

作業

  1. 赤い画面と緑の画面二つ用意する
  2. スペースキー押されたら別の画面にする

赤い画面と緑の画面二つ用意する

これをやるのは defscreen を二つ準備し、:on-key-down イベントで スペースが押されたら Screen 切替処理を実施すれば良い。

Screen を二つ用意する為のもっと具体的な手順は play-clj のチュートリアル に詳しいのでコードが雰囲気では良くわからない場合はそちらを見て頂きたい。

(declare arcade-clj-game main-screen alt-screen)

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    entities)

  :on-key-down
  (fn [screen entities]
    (condp = (:key screen)
      (key-code :space) (set-screen! arcade-clj-game alt-screen)   ;; (1)
      (key-code :escape) (app! :exit)
      entities))                                                   ;; (2)
  
  :on-render
  (fn [screen entities]
    (clear! 1 0 0 1)                                               ;; (3)
    (render! screen entities)))

(defscreen alt-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage)))

  :on-key-down
  (fn [screen entities]
    (condp = (:key screen)
      (key-code :space) (set-screen! arcade-clj-game main-screen) ;; (1)
      (key-code :escape) (app! :exit)
      entities))                                                  ;; (2)

  :on-render
  (fn [screen entities]
    (clear! 0 1 0 1)                                              ;; (3)
    (render! screen entities)))

ポイントは 3 箇所。

  1. space が押されたら自分とは異なる Screen への切替処理をここに記述
  2. はすいません、今回とは直接関係ないけど condp のデフォルト節を入れておくのを忘れてたのでここで修正しました。 前回のスナップショットだとこれがなかったせいで ESC 以外を押しても異常終了の形でアプリが終わってました。
  3. は Screen の背景色指定。RGB 指定。

ところでイベントハンドラに渡してる関数って Clojure 的にはタダの関数だよね

何が言いたいかと言うとこの段階だと 1,3 以外って同じだよなあと思い、 イベントハンドラ関数を外に出したコードを書いてみたんだが、 最初の画面は出るもののスペース押すと以下の例外吐いて落ちた。

Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.AssertionError: Assert failed: Attempted to set an invalid screen.

libGDX の AssertionError ですか。 まあ defscreen に渡す関数自体を使い回す事はほぼ無いだろうからここは深くは追わないようにしよう。 各スクリーンで処理がほぼ一緒に見えるのこういう段階だけだろうし。

defscreen もマクロでこれ評価すると実際は何か関数定義してるみたいだし。 コード追っかけてないが多分 reify なり proxy なりで Interface の実装クラス作ってるのは 何となく想像はつくけど。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.5.0 · shinmuro/arcade-clj · GitHub

一見かなりバージョンアップした風だけど、元記事と番号を合わせる為だけのタグなので悪しからず。

ESC を押したら終了するようにする(イベントハンドリングの触り)

Clojure play-clj arcade-clj

arcade-clj シリーズ その 2。シリーズ全体の目次はこちら

元記事での追加機能

  1. エスケープかxが押されたら終了するように変更する

以上。x は play-clj では特に何もしなくても既に終了するようになってるので割愛。 また、元記事ではライフタイムについても言及があるが Rust 特有の話なのでこれも割愛。

イベントハンドリング

元記事はメインループ回してポーリングでのイベントハンドリングから実装してるんだけど、 play-clj は defscreen からしてイベントドリブンな書き方を求めてるので 最初からイベントドリブンでやる事にする。

と言うか自分の最初の記事で 3 秒経ったら終了する機能もイベントドリブンで既に書いてたりする。

一応付け加えておくと、今回で 3 秒経ったら自動終了するコードは削除する。

:on-keydown

play-clj の defscreen の docを見ると キー押下時イベントは既に存在してるのでここに ESC が押されたら終了するようにすればいいのは 簡単に判断が付く。

変更した defscreen はこちら。

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    entities)

  :on-key-down
  (fn [screen entities]
    (condp = (:key screen)
      (key-code :escape) (app! :exit)))
  
  :on-render
  (fn [screen entities]
    (clear!)
    (render! screen entities)))

ええと、すいません、元記事的に追加した機能これだけだったんでこれで終わりです。 雰囲気で分かると思うけど一応補足すると、key-code に渡せるキーワード一覧も やはりplay-clj の key-code の docにあります。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.2.0 · shinmuro/arcade-clj · GitHub

play-clj のインストール、シンプルなウィンドウの表示

Clojure play-clj arcade-clj

arcade-clj シリーズ 1 つ目。シリーズ全体の目次はこちら。 一部以前の記事と重複している所もあるけどご容赦を。

play-clj のインストール

インストールと言うか leiningen での新規プロジェクト作成時にテンプレートで play-clj を指定するだけで良い。

lein new play-clj arcade-clj

プロジェクトが作成されたら desktop/project.cljClojure のバージョンを 1.8.0 に変更して lein deps すれば Clojure 含め必要なライブラリは全てインストールされる。 Clojure は別にこれと言って新しくする理由もないんだけど何となく。 *1

ひとまず動かしてみる

play-clj は lein run するととりあえず Window が出るまでの最低限のテンプレートを生成するので まずは lein run で動かしてみる。黒いウィンドウが出てきて左下に "Hello world!" と出ていれば OK。

元記事の仕様に合わせる

元記事は、

  1. タイトルに "ArcadeRS Shooter"
  2. 画面解像度 横 800, 縦 600
  3. 真っ黒な Window
  4. 3 秒経ったら自動的に終了する

となっているので合わせる。

まず、1,2 を合わせる為に desktop/src/arcade_clj/core/desktop_launcher.clj を開く。 そこの

(LwjglApplication. arcade-clj-game "arcade-clj" 800 600)

(LwjglApplication. arcade-clj-game "arcade-clj Shooter" 800 600)

とする。解像度は生成されたテンプレートのデフォルトと一致してたのでそのまま。

REPL で動かしてみる

さて、Clojure を使っていて REPL を使わない手はない。 コマンドプロンプトを立ち上げて lein repl :headless として nREPL サーバを起動する。 (cider-jack-in しない理由は後述する)。

次に emacs から desktop/src-common/arcade_clj/core.clj を開き cider-connect(C-c M-c)でさっき起動した nREPL サーバにつなぐ。

そして REPL から

arcade-clj.core.desktop-launcher> (-main)

とタイプし起動。project.clj を見ると分かるが、main は ns が desktop-launcher にあるので 別の ns で実行しても動作しない為注意。実行した結果、lein run した時と同じ画面が出てくると思う。

正直ここまでだと lein run の方が楽じゃないのと思うだろうがここからが違う。

じゃ次に元記事に仕様を合わせるべく次に画面を真っ黒にするためラベルを消してみる。 それは簡単で defscreen:on-show で指定されている label の S 式全体を消してしまえば良い。

  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage)))

こんな感じに。

で、ソースを保存し C-c C-k(cider-load-buffer) した後、nREPL クライアントで namespace を arcade-clj.core に移動した後で以下のコマンドを実行する。

(on-gl (set-screen! arcade-clj-game main-screen))

上記はざっくりと言えばゲーム画面に変更を反映させる為のコマンドになる。 このコマンドを実行すると画面から"Hello World!"はなくなり真っ黒になったはず。

それでもまだ面倒

正直自分が面倒だったのでさっきの手順をひとまとめに実行する emacs のコマンドを作った。 init.el 辺りにこんなコードを入れておく。

(defun play-clj-reload ()
  "REPL へ play-clj への反映コマンドを投げる。
   arcade-clj-game と main-screen は適時書き換える事。"
  (interactive)
  (cider-interactive-eval
   "(on-gl (set-screen! arcade-clj-game main-screen))"))

(defun save->eval->reload ()
  "play-clj での開発用。バッファセーブ→バッファ評価→play-cljのリロードのコンボをまとめて実行する。
   sit-for で wait かましているのはあんまり意味ないかも。"
  (interactive)
  (save-buffer)
  (sit-for 1)
  (cider-load-buffer)
  (sit-for 1)
  (play-clj-reload))
(define-key cider-mode-map (kbd "<f5>") 'save->eval->reload)

これで F5 キーを押すだけで「セーブ→セーブしたバッファの評価→ゲーム画面に反映」まで ひとまとめにやってくれる。

ただこれだとこの記事で作ったプロジェクト以外にする度に書き換える必要があるのが課題ではあるが どうすればもう少し汎用的になるかはまだ調べていない。

emacs の設定書き換えるの嫌なんですが

これはもう emacs から離れて NightMod 辺りを使うしかないと思う。 こちらは先のコマンド登録相当の事を機能として実装してるとの事なので試してみてはどうだろうか。

自分は NightMod 自体を試してない事から比較判断はできない。

最後の機能追加

さて、最後に 3 秒経ったら終了するコードを追加する。

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    (add-timer! screen :auto-dispose 3)           ;; (1)
    entities)                                     ;; ??

  :on-timer                                       ;; (2)
  (fn [screen entities]
    (case (:id screen)
      :auto-dispose (app! :exit)))
  
  :on-render
  (fn [screen entities]
    (clear!)
    (render! screen entities)))

これを評価すると 3 秒経った後画面が勝手に閉じる。 元記事と少し違ってタイマーを使って終了するようにした。

(1) で :auto-dispose と言う keyword で 3 秒後に実行されるタイマーを登録し、 タイマーが登録されると (2) の :on-timer イベントハンドラ関数が実行されるようになり、 screen の中にある :id が :auto-dispose ならアプリを終了するコードを追加している。

また、今回の機能追加に直接関係は無いが、?? の部分を補足する。

play-clj は各イベントハンドラ関数の戻り値としては entity が返される事を想定しており、 add-timer! 関数が返す Timer は entity としては扱えないので最後に持ってくるとエラーになる。 play-clj の leiningen テンプレートでは label を返すようになってたが、 これは entity として扱われるので OK だった。 とりあえず今回は怒られないようにする為だけにイベントハンドラで受け取った entities を そのまま返すようにした。中身は何も無いんだけどね。

entity system は play-clj 固有の概念なのだが、この辺りは 実際にもう少しちゃんと扱うようになったらまた改めて説明しようと思う。

play-clj REPL の制限

さて、アプリが自動終了した後 REPL でもう一度 (-main) として起動させようとして nil だけ返ってきてあれ? と思ったかもしれない。これは play-clj と言うか libGDX の制限で、 play-clj のゲーム画面と言うのは OpenGL コンテキストスレッド上で動作するのだが、 OpenGL コンテキストスレッドは 1 つのプロセスに対し 1 回しか起動を許していないという制限がある。

コマンドプロンプトの方を見るとそれらしき事を示す例外が吐き出されている。

Exception in thread "LWJGL Application" java.lang.RuntimeException: No OpenGL context found in the current thread.

自動終了するようコードを追加しておいてアレだが、アプリが終了した後は nREPL サーバを再起動しないともう起動できない。

nREPL サーバは jack-in しないで connect する事のススメ

また、この記事の最初の方でわざわざコマンドプロンプトを立ち上げて nREPL サーバを別に立ち上 げたのには理由があり、cider-jack-in した場合だと先のアプリが終了した時などの 例外を見えるようにする為というのが大きい。アプリ終了後に(-main)としても cider の nREPL クライアント側では nil としか返ってなかった事からも分かると思う。

例外が全く見えなくなってしまうのはさすがに辛いので play-clj で開発する際は コマンドプロンプトで nREPL サーバを立ち上げてから cider-connect する方法を強くオススメする。

また、アプリは終了せずとも :on-render などで大量に例外が吐き出されるような事も 開発中は起きてくるだろう(と言うか自分が体験した)。 例外が少ない場合はまだいいが、大量に例外が出続けると最初の方の例外はコマンドプロンプトだと 流れてこれも見えなくなってしまう。 このような場合は nREPL サーバ起動時にエラー出力をリダイレクトさせておくと良い。

> lein repl :headless 2> err.txt

こんな感じで。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.1.0 · shinmuro/arcade-clj · GitHub

*1:強いて上げれば 1.8.0 で追加された direct linking が play-clj で使えるのか試してみたいくらい