20200122のGoに関する記事は7件です。

Go makeとnewの違い

makeとnewの違い

違いはポインタを返すかどうか

make → ポインタを返さない

new → ポインタを返す

main.go
package main 

import(
    "fmt"
)

func main(){
    var p *int = new(int)
    fmt.Printf("%T\n", p) => *int

    s := make([]int, 0)
    fmt.Printf("%T\n", s) => []int

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

速習 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/learningGoCleanArchitecture

Clean 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

あの図

CleanArchitecture.jpg
via. The Clean Architecture
なんとなく分かったような分からないようなだ。役割ごとにレイヤーを分けて、依存関係をコントロールしていく感じだろうか。参考になる記事や書籍は世の中にたくさんあるので、詳細はそれらを読んでいただきたい。

学習方針

  1. Hello World
  2. 1枚ペラで簡単なAPIを作成
  3. Architecture に沿うように順々にバラしていく
  4. つまったら、都度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.go
package 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 などで立ち上げておいて欲しい。私は以下のようにした。

docker-compose のサンプル

最初の API

では、学習の元になる User API をペラ1で実装する。
ソースコードは記事が長くなるので、Github 参照。

ペラ 1 の main.go

こんなに簡単に書けるものを複雑にしていく必要は、このレベルであれば全くないと思うが、勉強のためと諦める。なお、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.go

Domain

このレイヤーでは、domain を表すのに必要なデータとメソッドを定義する。あの図の真ん中の黄色い部分。
どこにも依存しない。

├── app
├── domain
│   └── model
│       └── user.go (NEW!)
└── main.go (UPDATED!)

domain を定義

なお、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 に書く。

usecase/service: service

Repository を作る

あの図の右下の部分。
スクリーンショット 2020-01-22 9.51.02.png

依存性逆転の法則(DIP: Dependency Inversion Principle)の適用で実現している。はず。理解度が浅く、うまく説明できない。ので更なる学習が必要。コードを追ってやっていくと、自分が何がよく理解できていないのかが明らかになる。

├── domain
├── infrastructure
│   ├── api
│   └── datastore
│       └── userRepository.go (NEW!)
├── interface
├── usecase
│   ├── repository
│   │   └── userRepository.go (NEW!)
│   └── service
│       └── userService.go (UPDATED!)
└── main.go (UPDATED!)

Repository

presenter も同様のことをやるが、User API では利用しない。

UML を見てみる

ここまでで、諸々作業は残っているものの、構造的には、いったん Clean Architecture になった。
UMLを見てみると、

スクリーンショット 2020-01-20 18.24.35.png

こんな感じになっている。

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!)

validation

チェック

$ 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 Connection

この時点では、まだ呼び出していない。PostgreSQL に MySQL にあった Users テーブルと同じ構造のものを作っておけば、main.goNewMySQL としているところを NewPostgreSQL とするだけで切り替えることができる。最終的なコードでは registry の中で i.dbi.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.go

Property API

API の追加は、以下のような感じ。main.go は API を追加するだけなら変更は不要。今回は PostgreSQL を追加しているので変更されている。

infrastructure/api/handler/appHandler.go
package handler

type AppHandler struct {
    UserHandler
+   PropertyHandler
}
registry/registry.go
package 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.go
package 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件取得の方はそのままにした。

interface/presenter

チェック

$ 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 を”東京都”などと表示したいところだが、今回は息切れ。

都道府県コードでの検索を実装する

忘れていたので、付け足した。

search by pref

チェック。

$ 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

スクリーンショット 2020-01-22 11.50.47.png

最後に全部チェックしてみる。

## 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万円"
  }
]

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でレトロな感じの2Dゲームを作る

はじめに

昔Qiitaに、Go言語で作るマリオ風2Dゲームという記事を書きました。これを発展させる形で、昨年の中頃から会社の友人何人かと一緒に、Golangを使ってゲームを作り始めました。

私が昔、洞窟物語などのシンプルな2DのRPGゲームが好きだったこともあり、レトロさや手作り感を感じられるようなゲームの開発を目指してきました。最近になってこのゲーム開発の活動が停留してきたので、この記事でとりあえず現在までで出来たものをまとめてみます。

Githubのリポジトリはここです。

今のところ出来たもの

出来たもののgifをいくつか貼ります。背景画像およびプレーヤー画像などは仮にフリーの素材を用いたものであり、いずれはかっこいいものを自作したいと思ってます。

first.gif
作っているのは、こんな感じの2Dの俯瞰目線のゲームです。白い骸骨がプレーヤーで、青いお化けが敵キャラです。敵キャラの目が赤くなっているときは「追跡モード」で、プレーヤーを追いかけます(それ以外の時は、敵キャラはランダムな方向に移動しています)。

second.gif
骸骨は火の玉を撃って、敵を攻撃できます。

third.gif
ボスっぽいものも実装しました。ボスは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になっていたら、ランダムな時間にランダムな方向へ移動する動きをやめて、素早くプレーヤーに向かって動くようになります。コード全体はこちらです。

最後に

最近開発がバッタリと止まってしまいました。少しずつでもコーディングを進めて、何とか形にしていきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go log

logを学んだのでまとめておきます。

logのパッケージを使って出力すると

生年月日や時間が出力されます。

main.go
package main

import "log"

func main(){
   //logを出力
   log.Println("hello")

   //型や値を出力
   log.Printf("%T %v", "hello", "hello")

   //コードが終了する
   log.Fatalln("hello")
}

現時点で学んだ内容をまとめました。

随時更新していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go ファイルの読み取り

Goでファイルの読み取り方法を学んだのでまとめておきます。

main.go
package 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))
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang - goroutine(ゴルーチン) 】for - select パターン 例

概要

Golangにおけるゴルーチンのfor-selectパターンを実装してみました。
基本的には、チャネルを介してゴルーチン内外でデータのやり取り、エラーハンドリング、終了処理についての例を実装しています。

処理の流れは、

  1. ゴルーチン外部から、チャネルを介して文字を挿入("1", "bad0")
  2. ゴルーチン内部で、チャンネルから得た文字を数字に変換(エラーかどうか場合分けして、結果をresultに挿入)
  3. ゴルーチン外部から、doneをcloseすることで、ゴルーチンが終了し、それに伴いresultチャネルもcloseされる。
  4. ゴルーチン外部で、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 !!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む