- 投稿日:2020-01-22T22:29:46+09:00
Go makeとnewの違い
- 投稿日:2020-01-22T16:23:39+09:00
golangの名前付き戻り値に関して
名前付き変数はtour of goで紹介されているように、関数の最初で変数宣言したのと同様になる。
この時、nameの変数のスコープは
fugafuga()
に対して関数渡しで渡した関数から参照できるため、下記コードは問題なく動く。package main import "fmt" func fugafuga(f func() error, logger func(err error)) error { f() return nil } func hogehoge() (name string, err error) { fugafuga(func() error { name = "test" return nil }, func(err error) { }) return } func main() { n, _ := hogehoge() fmt.Println(n) }出力結果
$ go run main.go test
- 投稿日:2020-01-22T14:25:49+09:00
速習 Go + Clean Architecture
概要
Go 言語 + Clean Architecture を必要に迫られ勉強した。緊急だったので、Clean Architecture の概念をなめて、ネット上に散らばるソースコードを読んだ。が、ぶっちゃけよく分からなかったので、自分で書いてみた。3週くらいして、なんとなく分かった。気がする。
今回の学習のゴールは、チームのメンバー(同じく未経験)が理解できて、実装時に設計思想からはみ出ないようにすること。細かいことは抜きにして、読んでコピペすれば、何とかなる程度まで行ければ、細かい話はレビューで潰せば良い。という感じ。
Go も Clean Architecture も初めてな上、設計・実装からも遠ざかっていたので、理解に時間がかかった。完全に理解しているとは言い難いが、いちおうメモとして残しておく。実装は割と楽しかった。
作るもの
- go, clean architecture, net/http, gorm な REST API群
- 2 系統の CRUD API(User, Property)
- 2 系統の DB(MySQL, PostgreSQL)
最初、Gin で作ったがあまり必要性が感じられなかったので、net/http で作り直した。
ネット上の記事を読むと、go clean architecture な CRUD API の例はたくさん出てくる。が、1つ作って終わりのものが多い。Clean Architecture を理解するための記事なので、何個も作る必要はもちろんない。しかし、じゃあ実際に API を作るときに、1つだけの API なんてない訳で、どうやって2つ目の API 追加するの?とか、そういうのは、達人には自明でも、初心者は応用がきかない。ので、2つ作った。
また、DB は用途によって様々なものが用いられるので、2系統用意してみた。本当は、ユーザ情報のような少ないレコード数のものは、NoSQL、物件情報のような複雑で大量のレコード数のものは RDS というような使い分けが考えられるが(そもそも、そういうものを混ぜるなという話もあるが、規模次第)、Clean Architecture の学習からは外れるので、MySQL と PostgreSQL にした。正直あんまり意味はない。
最終的なソースコードは以下に置いてある。
https://github.com/kurab/learningGoCleanArchitectureClean Architecture とは
いちおう見ておく。
What is the Clean Architecture?
Clean architecture is a software design philosophy that separates the elements of a design into ring levels. The main rule of clean architecture is that code dependencies can only come from the outer levels inward. Code on the inner layers can have no knowledge of functions on the outer layers. The variables, functions and classes (any entities) that exist in the outer layers can not be mentioned in the more inward levels. It is recommended that data formats also stay separate between levels.
via. https://whatis.techtarget.com/definition/clean-architectureあの図
via. The Clean Architecture
なんとなく分かったような分からないようなだ。役割ごとにレイヤーを分けて、依存関係をコントロールしていく感じだろうか。参考になる記事や書籍は世の中にたくさんあるので、詳細はそれらを読んでいただきたい。学習方針
- Hello World
- 1枚ペラで簡単なAPIを作成
- Architecture に沿うように順々にバラしていく
- つまったら、都度1枚ペラで試してみる
最終的に Clean になれば、途中はいったん Clean じゃなくても気にしない。
API の仕様
USER API
- 全件取得
- 1件取得
- 登録
これはあちこちの解説で例として利用されている定番で、定番ゆえ迷った時に色々な記事と比較できるように、用意する。
不動産物件情報取得 API
- 全件取得
- 1件取得
- 条件マッチ取得
検索サービスにおいては、検索条件が1つだけなんてことはなく、様々な条件が組み合わさる。東京都、中野区で駅徒歩10分以内の賃料7.5万円以下25m2以上の賃貸マンションとか。としたいところだが、条件があったりなかったりをゴニョゴニョするのは、今回の趣旨とは若干離れるため、簡単に公開・非公開フラグと都道府県の組み合わせで検索をする。
また、不動産情報のようなものは、構造化されていて都道府県はコードで入っていたり、面積や価格などは単位のない数字として入っていることが多く、そのままでは使えないため、使いやすいように加工して返却する。
各 API の決め事
説明簡略化も含め、どのAPIも
- Create: /api/(api name like user)/register
- 条件付きRead: /api/(api name like user)/get/:id
- 全件取得Read: /api/(api name like user)/get
などとし、Update と Delete は今回は省略する。では、早速。
Hello, World.
main.gopackage main import ( "fmt" "net/http" "github.com/julienschmidt/httprouter" ) func setRouter() *httprouter.Router { router := httprouter.New() router.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { fmt.Fprintf(w, "Hello, world!") }) return router } func main() { r := setRouter() http.ListenAndServe(":8080", r) }チェック
$ go get github.com/julienschmidt/httprouter $ go run main.go $ curl -D - 0.0.0.0:8080 TTP/1.1 200 OK Date: Sun, 19 Jan 2020 04:20:06 GMT Content-Length: 13 Content-Type: text/plain; charset=utf-8 Hello, world!OK. 割と簡単なので、何とかなりそうな気がしてきた。Thank you, world!
User API
User は、Validation や暗号化をやりたいので、複雑になりすぎない程度で、id, name, age, email のフィールドを持たせる。暗号化は今回はやらないが気が向いたら追加する。
MySQLは、Docker などで立ち上げておいて欲しい。私は以下のようにした。
最初の API
では、学習の元になる User API をペラ1で実装する。
ソースコードは記事が長くなるので、Github 参照。こんなに簡単に書けるものを複雑にしていく必要は、このレベルであれば全くないと思うが、勉強のためと諦める。なお、Gin を使った場合、router 処理の長ったらしい引数や JSON の扱いがシンプルになる。(浅い!)
チェック
$ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"hiroshi", "age":43, "email":"hiroshi@foo.bar"}' $ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"john doe", "age":23, "email":"jonh@foo.bar"}' $ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"Alice", "age":18, "email":"alice@foo.bar"}' $ curl 0.0.0.0:8080/api/user/get | jq $ curl 0.0.0.0:8080/api/user/get/3 | jq諸々オッケー。以降、変化がない限りチェックは飛ばすが、都度やっている。
このペラ1を以下のような構造にバラしていく。├── config ├── domain //Enterprise Business Rules (Entities) │ └── model │ └── Users.go ├── infrastructure //framework, Drivers (External Interfaces) │ ├── api │ │ ├── handler │ │ ├── middleware │ │ ├── router │ │ └── validater │ └── datastore ├── interface //Interface Adapters (Controller, Gateway, Presenter) │ ├── controllers │ └── presenters ├── registry ├── usecase //Application Business Rules (Use Case) │ ├── presenter │ ├── repository │ └── service └── main.goDomain
このレイヤーでは、domain を表すのに必要なデータとメソッドを定義する。あの図の真ん中の黄色い部分。
どこにも依存しない。├── app ├── domain │ └── model │ └── user.go (NEW!) └── main.go (UPDATED!)なお、go module を利用しており、終始
$ go mod init api $ go build $ go run apiなどとしている。コード中の実装した package の import は、github のパスにはなっていない。
Infrastructure
外と中をつなぐレイヤーで、Interface Adapter に依存する。あの図の一番外の青い輪っか。
DB Connection
├── domain ├── infrastructure │ └── datastore │ └── dbMySQL.go (NEW!) └── main.go (UPDATED!)▶ infrastructure/datastore: db connection
DBに接続する部分を infrastructure に切り出した。
切り出して main.go から呼んでいるだけ。Router
├── domain ├── infrastructure │ ├── api │ │ └── router │ │ └── router.go (NEW!) │ └── datastore └── main.go (UPDATED!)▶ infrastructure/api/router: router
同様にして Routing の処理を切り出す。Handler
├── domain ├── infrastructure │ └── api │ ├── handler │ │ └── userHandler.go (NEW!) │ └── router │ └── router.go (UPDATED!) └── main.go (UPDATED!)▶ infrastructure/api/handler: handler
Router は Routing だけに集中してもらい、中身の処理を handler に移した。
ここで、この後も散々出てくるが、以下のような interface, struct, NewXX を用いることで、UserHandler を利用する側が、UserHandler の Interface だけを知っている状態(内部実装は知らなくて良い)とする。こんな感じで依存関係を考えていく。 Dependency Injection Pattern を調べると良い。type UserHandler interface { CreateUser(...) GetAllUsers(...) GetUser(...) } type userHandler struct { db *gorm.DB } func NewUserHandler(db *gorm.DB) UserHandler { return &userHandler{db: db} }Interface Adapter
Interface Adapter は、Infrastructure と Usecase をつなぐレイヤーで、
Controller では外から受け取った値を内部的に使いやすいようにする。
Presenter では、内部から受け取ったデータを外で使いやすいようにする。
Usecase に依存する。真ん中の緑の輪っかの部分。controller
├── domain ├── infrastructure │ ├── api │ │ ├── handler │ │ │ └── userHandler.go (UPDATED!) │ │ └── router │ └── datastore ├── interface │ └── controllers │ └── userController.go (NEW!) └── main.go (UPDATED!)handler の処理を、controller, usecase にばらしていく。まずは、controller
▶ interface/controllers: controllers
Presenter は USER API では実装しない。
Usecase
ビジネスロジックを定義するレイヤー。Domain に依存する。
ピンクの部分。Service
├── domain ├── infrastructure ├── interface │ └── controllers │ └── userController.go (UPDATED!) ├── usecase │ └── service │ └── userService.go (NEW!) └── main.go (UPDATED)controller には、技術的に何がしたいのかだけが書かれているイメージ。具体的なビジネスロジックを usecase に書く。
Repository を作る
依存性逆転の法則(DIP: Dependency Inversion Principle)の適用で実現している。はず。理解度が浅く、うまく説明できない。ので更なる学習が必要。コードを追ってやっていくと、自分が何がよく理解できていないのかが明らかになる。
├── domain ├── infrastructure │ ├── api │ └── datastore │ └── userRepository.go (NEW!) ├── interface ├── usecase │ ├── repository │ │ └── userRepository.go (NEW!) │ └── service │ └── userService.go (UPDATED!) └── main.go (UPDATED!)presenter も同様のことをやるが、User API では利用しない。
UML を見てみる
ここまでで、諸々作業は残っているものの、構造的には、いったん Clean Architecture になった。
UMLを見てみると、こんな感じになっている。
Registry
main.go で、依存関係をズラズラ書いてきたが、今後もどんどん増えるので、切り離す。
├── domain ├── infrastructure ├── interface ├── registry │ └── registry.go (NEW!) ├── usecase └── main.go (UPDATED!)▶ registry
これはこれで分かりやすいが、長くなるので気に入らない。いずれ考える。
Validation
Validation は、infrastructure レイヤーでやる。
github.com/go-playground/validator/v10
を使った。├── config ├── domain │ └── model │ └── user.go (UPDATED!) ├── infrastructure │ ├── api │ │ ├── handler │ │ │ └── userHandler.go (UPDATED!) │ │ ├── router │ │ └── validator │ │ └── validater.go (NEW!) │ └── datastore ├── interface ├── registry │ └── registry.go (UPDATED!) ├── usecase └── main.go (UPDATED!)チェック
$ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"", "age":300, "email":"@foobar"}' Validation error: Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lt' tag Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tagちゃんと Validation できてる。
Config
dbMySQL.go に接続情報がベタ書きなので、設定ファイルに切り出す。環境変数に格納することも多いと思うので、それにも対応しておく。
├── config │ ├── config.go (NEW!) │ └── config.yml (NEW!) ├── domain ├── infrastructure │ ├── api │ └── datastore │ └── dbMySQL.go (UPDATED!) ├── interface ├── registry ├── usecase └── main.go (UPDATED!)▶ config
やっと、それっぽい User API が完成した。
Property API
DB は User API で利用した MySQL ではなく、PostgreSQL を利用する。最初に提示した docker-compose に入れてある。
Properties テーブルは、
item type description id int 物件ID flg_open bool 公開・非公開フラグ pref_cd smallint 都道府県コード walk_time int 駅徒歩分 area numeric(10,2) 面積 price int 価格 とし、予め以下のデータを入れておく。
id flg_open pref_cd walk_time area price 1 ture 13 10 70.50 100000000 2 true 14 15 100.00 50000000 3 false 13 5 50.15 60000000 4 true 13 7 80.00 120000000 API を Call した際に、Response では、駅徒歩分には”分”、面積には"m2"を付け、価格は、10,000で割って”万円”を付けて返却する。
walk_time, area, price は DB 上は整数・実数で格納されているが、API 上は文字列として扱いたいので、以下で示す model ではstring
としている。ただし、これはビジネス・ロジックのような気がするので、Domain に入れるべきではないかも知れない。検索結果は、1件取得を除き、指定条件(今回はpref_cdまたはなし)+公開フラグが立っている(flg_open=ture)ものを都道府県コードの昇順、価格の昇順(安い)もので並べる。
都道府県コードが13番で検索した場合は、1, 4, 2 の順で検索されて 3 は検索されない。というものを作る。PostgreSQL Connection
Gorm で PostgreSQL にも接続できるようにする。
├── config │ ├── config.go (UPDATED!) │ └── config.yml (UPDATED!) ├── domain │ └── model │ ├── property.go (NEW!) │ └── user.go ├── infrastructure │ ├── api │ └── datastore │ ├── dbMySQL.go │ ├── dbPostgreSQL.go (NEW!) │ └── userRepository.go ├── interface ├── registry ├── usecase └── main.go (UPDATED!)この時点では、まだ呼び出していない。PostgreSQL に MySQL にあった Users テーブルと同じ構造のものを作っておけば、
main.go
でNewMySQL
としているところをNewPostgreSQL
とするだけで切り替えることができる。最終的なコードでは registry の中でi.db
をi.ps
とするだけ。DB の接続部分だけ作れば、後はプログラム的には MySQL だろうが PostgreSQL だろうがどうでも良い。Property API を実装する
大体コピペで行ける。コピペの勢いが付きすぎて、都道府県コードによる条件検索を忘れた。最後につける。
├── config ├── domain ├── infrastructure │ ├── api │ │ ├── handler │ │ │ ├── appHandler.go (UPDATED!) │ │ │ ├── propertyHandler.go (NEW!) │ │ │ └── userHandler.go │ │ ├── router │ │ │ └── router.go (UPDATED!) │ │ └── validator │ └── datastore │ ├── dbMySQL.go │ ├── dbPostgreSQL.go │ ├── propertyRepository.go (NEW!) │ └── userRepository.go ├── interface │ └── controllers │ ├── propertyController.go (NEW!) │ └── userController.go ├── registry │ ├── registry.go (UPDATED!) ├── usecase │ ├── repository │ │ ├── propertyRepository.go (NEW!) │ │ └── userRepository.go │ └── service │ ├── propertyService.go (NEW!) │ └── userService.go └── main.goAPI の追加は、以下のような感じ。main.go は API を追加するだけなら変更は不要。今回は PostgreSQL を追加しているので変更されている。
infrastructure/api/handler/appHandler.gopackage handler type AppHandler struct { UserHandler + PropertyHandler }registry/registry.gopackage registry ... type Interactor interface { NewAppHandler() handler.AppHandler } func NewInteractor(db *gorm.DB, ps *gorm.DB, v *validator.Validate) Interactor { return &interactor{db: db, postgres: ps, validator: v} } func (i *interactor) NewAppHandler() handler.AppHandler { return handler.AppHandler{ UserHandler: i.NewUserHandler(), PropertyHandler: i.NewPropertyHandler()} } ...main.gopackage main ... func main() { ... h := rg.NewAppHandler() ... }では、チェック
$ curl 0.0.0.0:8080/api/property/get | jq [ { "id": 1, "flg_open": true, "pref_cd": 13, "walk_time": "10", "area": "70.50", "price": "100000000" }, { "id": 4, "flg_open": true, "pref_cd": 13, "walk_time": "7", "area": "80.00", "price": "120000000" }, { "id": 2, "flg_open": true, "pref_cd": 14, "walk_time": "15", "area": "100.00", "price": "50000000" } ] $ curl 0.0.0.0:8080/api/property/get/3 | jq { "id": 3, "flg_open": false, "pref_cd": 13, "walk_time": "5", "area": "50.15", "price": "60000000" }思ったように並んでいる。
Presenter で加工する
単位を付けたり円を万円にしたりする。
├── config ├── domain ├── infrastructure ├── interface │ ├── controllers │ └── presenters │ └── propertyPresenter.go (NEW!) ├── registry │ ├── registry.go (UPDATED!) ├── usecase │ ├── presenter │ │ └── propertyPresenter.go (NEW!) │ ├── repository │ │ ├── propertyRepository.go │ │ └── userRepository.go │ └── service │ ├── propertyService.go (UPDATED!) │ └── userService.go └── main.go違いを見るために、全件取得の方だけ加工し、1件取得の方はそのままにした。
チェック
$ curl 0.0.0.0:8080/api/property/get | jq [ { "id": 1, "flg_open": true, "pref_cd": 13, "walk_time": "10分", "area": "70.50m2", "price": "10000万円" }, { "id": 4, "flg_open": true, "pref_cd": 13, "walk_time": "7分", "area": "80.00m2", "price": "12000万円" }, { "id": 2, "flg_open": true, "pref_cd": 14, "walk_time": "15分", "area": "100.00m2", "price": "5000万円" } ] $ curl 0.0.0.0:8080/api/property/get/3 | jq { "id": 3, "flg_open": false, "pref_cd": 13, "walk_time": "5", "area": "50.15", "price": "60000000" }できました。本来であれば、pref_cd = 13 を”東京都”などと表示したいところだが、今回は息切れ。
都道府県コードでの検索を実装する
忘れていたので、付け足した。
チェック。
$ curl 0.0.0.0:8080/api/property/pref/get/13 | jq [ { "id": 1, "flg_open": true, "pref_cd": 13, "walk_time": "10分", "area": "70.50m2", "price": "10000万円" }, { "id": 4, "flg_open": true, "pref_cd": 13, "walk_time": "7分", "area": "80.00m2", "price": "12000万円" } ]OYAY!
最終的な UML
最後に全部チェックしてみる。
## USER 登録 $ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"Prakruti", "age":20, "email":"prakruti@foo.bar"}' registerd ## USER 登録 Validation error $ curl 0.0.0.0:8080/api/user/register curl -X POST -H "Content-Type: application/json" -d '{"name":"", "age":300, "email":"foobar"}' Validation error: Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lt' tag Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag ## USER 全件取得 $ curl 0.0.0.0:8080/api/user/get | jq [ { "id": 1, "name": "hiroshi", "age": 43, "email": "hiroshi@foo.bar" }, { "id": 2, "name": "john doe", "age": 23, "email": "jonh@foo.bar" }, { "id": 3, "name": "Alice", "age": 18, "email": "alice@foo.bar" }, { "id": 4, "name": "robert", "age": 30, "email": "robert@foo.bar" }, { "id": 5, "name": "Mary", "age": 21, "email": "mary@foo.bar" }, { "id": 6, "name": "", "age": 43, "email": "hiroshi@foo.bar" }, { "id": 7, "name": "Maricruz", "age": 30, "email": "maricruz@foo.bar" }, { "id": 8, "name": "Prakruti", "age": 20, "email": "prakruti@foo.bar" } ] ## USER 1件取得 $ curl 0.0.0.0:8080/api/user/get/3 | jq { "id": 3, "name": "Alice", "age": 18, "email": "alice@foo.bar" ## PROPERTY 全件取得 $ curl 0.0.0.0:8080/api/property/get | jq [ { "id": 1, "flg_open": true, "pref_cd": 13, "walk_time": "10分", "area": "70.50m2", "price": "10000万円" }, { "id": 4, "flg_open": true, "pref_cd": 13, "walk_time": "7分", "area": "80.00m2", "price": "12000万円" }, { "id": 2, "flg_open": true, "pref_cd": 14, "walk_time": "15分", "area": "100.00m2", "price": "5000万円" } ] ## PROPERTY 1件取得 $ curl 0.0.0.0:8080/api/property/get/3 | jq { "id": 3, "flg_open": false, "pref_cd": 13, "walk_time": "5", "area": "50.15", "price": "60000000" } ## PROPERTY 条件マッチ取得 $ curl 0.0.0.0:8080/api/property/pref/get/13 | jq [ { "id": 1, "flg_open": true, "pref_cd": 13, "walk_time": "10分", "area": "70.50m2", "price": "10000万円" }, { "id": 4, "flg_open": true, "pref_cd": 13, "walk_time": "7分", "area": "80.00m2", "price": "12000万円" } ]おしまい。
- 投稿日:2020-01-22T11:34:31+09:00
Go言語でレトロな感じの2Dゲームを作る
はじめに
昔Qiitaに、Go言語で作るマリオ風2Dゲームという記事を書きました。これを発展させる形で、昨年の中頃から会社の友人何人かと一緒に、Golangを使ってゲームを作り始めました。
私が昔、洞窟物語などのシンプルな2DのRPGゲームが好きだったこともあり、レトロさや手作り感を感じられるようなゲームの開発を目指してきました。最近になってこのゲーム開発の活動が停留してきたので、この記事でとりあえず現在までで出来たものをまとめてみます。
今のところ出来たもの
出来たもののgifをいくつか貼ります。背景画像およびプレーヤー画像などは仮にフリーの素材を用いたものであり、いずれはかっこいいものを自作したいと思ってます。
作っているのは、こんな感じの2Dの俯瞰目線のゲームです。白い骸骨がプレーヤーで、青いお化けが敵キャラです。敵キャラの目が赤くなっているときは「追跡モード」で、プレーヤーを追いかけます(それ以外の時は、敵キャラはランダムな方向に移動しています)。
ボスっぽいものも実装しました。ボスは8方向に火の玉を出します。下のほうにある緑の棒が、ライフバーです。コードについて
使ったライブラリ
engoというライブラリを使いました。このライブラリでは、"System"という構造体を使ってエンティティ―(プレーヤー、敵、ボスとか)を作成します。"System"にはデフォルトで、ロード時に呼ばれる"New"関数、毎フレーム呼ばれる"Update"関数、そしてゲームからの削除時に呼ばれる"Remove"関数が用意されています。
こんな感じです。
// SomeSystem なんかのシステム type SomeSystem struct { world *ecs.World texture *common.Texture } func (ss *SomeSystem) New(w *ecs.World) { // 新規作成時の処理をここに書く } func (ss *SomeSystem) Update(dt float32) { // 毎フレーム行う処理をここに書く } func (ss *SomeSystem) Remove(entity ecs.BasicEntity) { // 削除時の処理をここに書く }大変だったところ
ゲーム開発を行ったメンバーは皆、ゲーム制作はもちろんプログラミングやデザインに関しても素人であったので、いろいろ苦労しました。下に、実装において大変だったことをいくつか羅列していきます。
背景の描画
将来的には背景画像を自作したいと考えているのですが、当面はオンライン上で拾ってきた素材画像を使用します。このサイトにあるやつとかです。これらの画像を、細かく切り出して画面に張り付けることで、背景を描画しています。
その際、どの位置にどの画像を張るかの情報を、json形式のファイルに保持するようにしました。こんな感じです。
{ "meta-data":{ "id":0, "player-initial-positions":{ "A":{"X":5,"Y":5}, "B":{"X":26,"Y":32} }, "camera-initial-positions":{ "A":{"X":300,"Y":200}, "B":{"X":700,"Y":900} }, "boss-fight":0, "spritesheet":"pics/overworld_tileset_grass.png" }, "cell-data":[ [ { "cell":95, "portal":false, "obstacle":true, "enemy": false },{ "cell":95, "portal":false, "obstacle":true, "enemy": false },{ "cell":95, "portal":false, "obstacle":true, "enemy": false },{ "cell":95, "portal":false, "obstacle":true, "enemy": false },{ "cell":95, "portal":false, "obstacle":true, "enemy": false },{ "cell":95, "portal":false, "obstacle":true, "enemy": false }, [セルの数だけこれが続く...] ] ] }"meta-data"の中でステージのメタ情報(プレーヤーが最初どの位置にいるべきか、ボス戦であるかどうか、など)を保持して、"cell-data"の二重配列の中で、各セルごとの情報(描画するべき画像、通り抜けられるかどうか、敵がいるかどうかなど)を保持します(実物はGithubにあります)。
このjsonファイルを、必要な時に読み込んでいきます。ただし、Go言語でjsonファイルを読み込むのはかなり大変です。jsonファイルと同じ形式の構造体を作っておいてもいいのですが、それだと柔軟にファイルに情報を付け足したりしにくくなるので、"interface{}"として読み取ってます。この部分のコードを一部抜粋すると、こんな感じです。
// jsonファイルの読み込み file, err := os.Open(stageFileToRead) if err != nil { fmt.Println(err) } defer file.Close() byteValue, _ := ioutil.ReadAll(file) // 頑張ってjsonファイルを読み込む var sceneJSON map[string]interface{} json.Unmarshal([]byte(byteValue), &sceneJSON) // プレーヤーの初期位置情報を取得 playerInitialPositionX = int(sceneJSON["meta-data"].(map[string]interface{})["player-initial-positions"].(map[string]interface{})["A"].(map[string]interface{})["X"].(float64)) playerInitialPositionY = int(sceneJSON["meta-data"].(map[string]interface{})["player-initial-positions"].(map[string]interface{})["A"].(map[string]interface{})["Y"].(float64)) // カメラ(視点)の初期位置情報を取得 cameraInitialPositionX = int(sceneJSON["meta-data"].(map[string]interface{})["camera-initial-positions"].(map[string]interface{})["A"].(map[string]interface{})["X"].(float64)) cameraInitialPositionY = int(sceneJSON["meta-data"].(map[string]interface{})["camera-initial-positions"].(map[string]interface{})["A"].(map[string]interface{})["Y"].(float64)) // ボス戦かどうかの情報を取得 if int(sceneJSON["meta-data"].(map[string]interface{})["boss-fight"].(float64)) == 1 { ifBossFight = true } else { ifBossFight = false }見ての通り、力技で頑張っています(Github上のコード)。
現在は、上に挙げた形式のjsonファイルを読み込ませれば、とりあえずステージは作れるようになっています。
シーンの切り替え
プレーヤーが特定の位置(ドアとか)に移動したら、シーンを切り替えて、背景画像を更新したり、敵のエンティティ―を作成したり、場合によってはボス戦を開始します。
これの実装方法に関して結構悩んだのですが、最終的には"intermission"という、シーンの切り替えを表す"System"を作成することにしました。この"System"では、"New"関数で画面に黒い幕を下ろし、"Update"関数でシーン切り替えの処理をして、"Remove"関数で幕を取り外すよう実装してあります。
// New 新規作成 func (is *IntermissionSystem) New(w *ecs.World) { is.world = w // 画面を黒く覆う shadePic, _ = common.LoadedSprite("pics/black_bk.png") shadingProgress = 0 intermissionState = false } // Remove 削除時の関数 func (is *IntermissionSystem) Remove(entity ecs.BasicEntity) { for _, system := range is.world.Systems() { switch sys := system.(type) { case *common.RenderSystem: // 自分自身のデータを削除 sys.Remove(entity) } } } // Update 毎フレームの処理 func (is *IntermissionSystem) Update(dt float32) { // いろいろやってるけど、長いので省略 // 新しいステージを読み込んだりしています // 詳細はGitHubのリポジトリを見てください }Github上の、完全版のコードはこちら。
プレーヤーの攻撃
プレーヤーは火の玉を撃てます。この火の玉ですが、単一の画像では面白くないので、大きくなったり小さくなったりして、動きを出すようにしました。そのため、火の玉のエンティティーは自分が作成されてからどれくらいの時間が経ったかの情報を保持して、それに応じて自身の画像を切り替えています。
// 毎フレーム呼ばれる処理 func (bs *BulletSystem) Update(dt float32) { // 自身の年齢を更新 bullet.bulletPicChangeCounter++ // 自身の年齢に応じて、描画する画像を取得 bulletPicIndex := bullet.bulletPicChangeCounter / 5 // 寿命が来たら、ステージから削除 if bulletPicIndex > 7 { bs.Remove(bullet.BasicEntity) bulletEntities = removePlayerBullet(bulletEntities, bullet) continue } // 寿命が来てなかったら、画像を更新 bullet.RenderComponent.Drawable = bulletPics[bulletPicIndex] }コード全体はこちらです。
敵の動き
プレーヤーが遠くにいるとき、敵はランダムな動きをします。しかし、プレーヤーが近づいたときには、プレーヤーめがけて移動するようになります。こんな感じの実装です。
// 敵とプレーヤーのチェビシェフ距離 distance := math.Abs(float64(playerInstance.cellX - o.cellX)) if distance < math.Abs(float64(playerInstance.cellY-o.cellY)) { distance = math.Abs(float64(playerInstance.cellY - o.cellY)) } if distance < enragedDistance { // 距離が一定以内なら、追跡モードにする o.mode = 1 } else { // そうでなければ、追跡モードにしない o.mode = 0 }
mode
が1になっていたら、ランダムな時間にランダムな方向へ移動する動きをやめて、素早くプレーヤーに向かって動くようになります。コード全体はこちらです。最後に
最近開発がバッタリと止まってしまいました。少しずつでもコーディングを進めて、何とか形にしていきたいです。
- 投稿日:2020-01-22T09:27:47+09:00
Go log
logを学んだのでまとめておきます。
logのパッケージを使って出力すると
生年月日や時間が出力されます。
main.gopackage main import "log" func main(){ //logを出力 log.Println("hello") //型や値を出力 log.Printf("%T %v", "hello", "hello") //コードが終了する log.Fatalln("hello") }現時点で学んだ内容をまとめました。
随時更新していきます。
- 投稿日:2020-01-22T09:04:19+09:00
go ファイルの読み取り
Goでファイルの読み取り方法を学んだのでまとめておきます。
main.gopackage main import( "fmt" "os" ) func main() { //file変数にファイルのデータを代入 //ここではエラーは空白に file, _:= os.Open("ファイル名") //ファイルのクローズを最後に行うためのdefer defer file.Close() //バイト配列を作成 data := make([]byte, 100) //バイト配列にファイルのデータを読み込む file.Read(data) //バイト配列に格納されたデータを出力 //バイト型になっているので、string()で文字列に型変換を行う fmt.Println(string(data)) }
- 投稿日:2020-01-22T03:53:23+09:00
【Golang - goroutine(ゴルーチン) 】for - select パターン 例
概要
Golangにおけるゴルーチンのfor-selectパターンを実装してみました。
基本的には、チャネルを介してゴルーチン内外でデータのやり取り、エラーハンドリング、終了処理についての例を実装しています。処理の流れは、
- ゴルーチン外部から、チャネルを介して文字を挿入("1", "bad0")
- ゴルーチン内部で、チャンネルから得た文字を数字に変換(エラーかどうか場合分けして、結果をresultに挿入)
- ゴルーチン外部から、doneをcloseすることで、ゴルーチンが終了し、それに伴いresultチャネルもcloseされる。
- ゴルーチン外部で、resultを取得していたforが終了する。
実装
以下にプログラムと結果を示します。
package main import ( "fmt" "strconv" "time" ) type Result struct { Error error Response int } func doSomething(done <-chan interface{}, printStr <-chan string) <-chan Result { result := make(chan Result) go func() { defer fmt.Println("タスク終了") defer close(result) for { select { case s := <-printStr: i, err := strconv.Atoi(s) if err != nil { result <- Result{Error: err, Response: 0} } else { result <- Result{Error: err, Response: i} } case <-done: return } } }() return result } func main() { done := make(chan interface{}) printStr := make(chan string) result := doSomething(done, printStr) //1秒後にdoSomethingを終了 go func() { pushData := []string{"1", "bad1"} for _, v := range pushData { printStr <- string(v) } time.Sleep(1 * time.Second) fmt.Println("doSomethingを終了") close(done) }() //doSomething終了に伴いresultがcloseされる。 for res := range result { if res.Error != nil { fmt.Println("error:", res.Error) continue } fmt.Println("Result:", res.Response) } fmt.Println("Finished !!") }Result: 1 error: strconv.Atoi: parsing "bad1": invalid syntax doSomethingを終了 タスク終了 Finished !!