🍤

Goで物理演算

2024/08/19に公開

jakecoffman/cp という物理演算を行うためのGoのパッケージがありまして、これはChipmunk2DというC言語で書かれた物理演算ライブラリをGoに移植したものです。今回はこのパッケージを使った物理演算をする方法を説明しようと思います。

物理演算というと難しそうなイメージがありますが、やってみると意外と簡単なのでご興味がある方は最後までお読み頂ければとおもいます。

最低限の知識

Chipmunk2Dを使う上で最低限必要な知識は次の3つです。

  1. Body
    • Bodyはmass(質量), position(位置), rotation(回転), velocity(速度)などを持ちます
    • Bodyには形はありません、Bodyの形はShapeを使って設定します
  2. Shape
    • 形状を設定します
    • 形状の他、摩擦・弾力性などのオブジェクトの表面特性も設定します
    • BodyにShapeを接続することでBodyの形状を設定します
  3. Space
    • SpaceにBody、Shapeを接続することで物理演算を行います

この3つを覚えておくだけで後は何とかなります。

ボールを定義するサンプルコード

ではさっそくコードを見ていきましょう。
次のサンプルコードは SpaceBodyShape を追加するaddBallという関数です。

ちょっと長いですが、ここが最大の山場なのでここを理解できさえすればこれ以降はそれほど難しくはありません。

func addBall(space *cp.Space, x, y, radius float64) *cp.Body {
	// 質量
	mass := radius * radius / 100.0
	// BodyをSpaceに追加(質量、モーメントも設定している)
	body := space.AddBody(
		cp.NewBody(
			mass,
			cp.MomentForCircle(mass, 0, radius, cp.Vector{}),
		),
	)
	// Bodyの位置を設定
	body.SetPosition(cp.Vector{X: x, Y: y})
	// ShapeをBodyに接続しつつ、ShapeをSpaceに追加
	shape := space.AddShape(
		cp.NewCircle(
			body,
			radius,
			cp.Vector{},
		),
	)
	// 弾性を設定
	shape.SetElasticity(0.5)
	// 摩擦を設定
	shape.SetFriction(0.5)
	return body
}

続いてSpaceの設定を行うのと、先ほど作ったddBall関数を使ってSpaceにボールを追加します。

package main

import (
	_ "image/png"

	"github.com/jakecoffman/cp/v2"
)

var (
	space *cp.Space
)

func main() {
	space = cp.NewSpace()
	// 重力を設定
	space.SetGravity(cp.Vector{X: 0, Y: -100})
	// ボールをSpaceに追加する。このときの座標は(X:0, Y:0)で、半径が50。
	ball := addBall(space, 0, 0, 50)
	// 1/60秒分だけ動かす
	// (通常は1回実行するだけで良いが、今回の例だと2回呼び出す必要がある)
	space.Step(1 / 60.0)
	space.Step(1 / 60.0)
	// 移動後の座標を標準出力に出力する
	println(ball.Position().X) // +0.000000e+000
	println(ball.Position().Y) // -2.777778e-002
}
func addBall(space *cp.Space, x, y, radius float64) {
	// 略
}

これで設定は終わりです!

これを実行すると、main関数の最後の2行で物理演算の結果によりボールが移動した後の座標が出力されます。意外ですがたったこれだけで物理演算ができています。しかし演算結果が標準出力に出力されるだけではつまらないですよね。

ということで今回はEbitengineを使って画面に表示しようと思います。

Ebitengine で描画

Ebitengine を使うよう次のように書き換えます。

Update(), Draw(), Layout() という3つのメソッドを定義した Game{}ebiten.RunGame() に渡しています。 Update() では物理演算を行なっていて、 Draw() では演算された座標をもとに描画を行なっています。

  package main

  import (
  	_ "image/png"

+ 	"github.com/demouth/ebitencp"
+ 	"github.com/hajimehoshi/ebiten/v2"
  	"github.com/jakecoffman/cp/v2"
  )

+ const (
+ 	screenWidth  = 640
+ 	screenHeight = 480
+ )

  var (
  	space  *cp.Space
+ 	drawer *ebitencp.Drawer
  )

+ type Game struct{}

+ func (g *Game) Update() error {
 	// 1/60秒分だけ動かす
 	space.Step(1 / 60.0)
+ 	return nil
+ }
+ func (g *Game) Draw(screen *ebiten.Image) {
+ 	// 描画ライブラリでChipmunk2DのSpaceをEbitengineを使って描画する
+ 	cp.DrawSpace(space, drawer.WithScreen(screen))
+ }
+ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
+ 	return screenWidth, screenHeight
+ }
  func main() {
  	space = cp.NewSpace()
  	// 重力を設定
  	space.SetGravity(cp.Vector{X: 0, Y: -100})
  	// ボールをSpaceに追加する。このときの座標は(X:0, Y:0)で、半径が50。
  	addBall(space, 0, 0, 50)
+ 	game := &Game{}
+ 	drawer = ebitencp.NewDrawer(screenWidth, screenHeight)
+ 	ebiten.SetWindowSize(screenWidth, screenHeight)
+ 	ebiten.RunGame(game)
  }
  func addBall(space *cp.Space, x, y, radius float64) *cp.Body {
  	// 略
  }

これを実行すると次のようになります。

ボールが画面に表示されるようになりましたが、重力により下に進み、そのまま画面の外にはみ出してしまいました。これではつまらないので、床を作って床に落ちるようにしてみます。

床を作る

addWall() という関数を作ってみました。この関数に2組のXY座標を渡すことで、その位置に線状の床を作ることができます。

+ func addWall(space *cp.Space, x1, y1, x2, y2, radius float64) {
+ 	pos1 := cp.Vector{X: x1, Y: y1}
+ 	pos2 := cp.Vector{X: x2, Y: y2}
+ 	shape := space.AddShape(cp.NewSegment(space.StaticBody, pos1, pos2, radius))
+ 	shape.SetElasticity(0.5)
+ 	shape.SetFriction(0.5)
+ }
 func main() {
 	// 略
 	addBall(space, 0, 0, 50)
+	addWall(space, -200, -200, 200, -200, 5)
 	// 略
 }

実行すると次のようになります。床でボールが跳ねるようになりましたね。

床とボールを沢山作る

ここまでで作った addBalladdWall という関数を何度か呼びだすように書き換えるだけでこんな感じできます。

	addBall(space, 0, 0, 50)
	addBall(space, 10, 200, 20)
	addWall(space, -300, 200, -300, -200, 5)
	addWall(space, 300, 200, 300, -200, 5)
	addWall(space, -300, -200, 300, -200, 5)

面白いですね!

ドラッグできるようにする

そして次の1行を追加すると、ドラッグをできるようになります。

 func (g *Game) Update() error {
+	drawer.HandleMouseEvent(space)
 	// 1/60秒分だけ動かす
 	space.Step(1 / 60.0)
 	return nil
 }

ここまでで作ったソースコードの全体像は下記で確認できます。

https://github.com/demouth/ebiten-chipmunk/blob/main/examples/basic/main.go

描画処理を工夫する

ここまでの内容を応用して、ボールの位置座標にEbitengineでドット絵を出力するようにしてみました。

Ebitengineを使っているのでブラウザでも動かすことができ、次のURLで実際に確認できます。
https://demouth.github.io/ebitengine-sketch/008/

もっと複雑な物理演算

Chipmunk2Dを使うと次のようにテオ・ヤンセンのストランドビーストのような複雑なものも動かすことができます。

こちらについても次のURLから実際にブラウザで動かすことができます。

https://demouth.github.io/ebitengine-sketch/011/

ゲームに応用する

次の例はスイカゲーム風のパズルゲームに応用した例です。物理演算ライブラリを使うことで、単純な丸い形だけではなく複雑な形を使ったパズルを実現しています。

こちらについても次のURLから実際にブラウザで動かすことができます。

https://demouth.github.io/ebitengine-sketch/013/

おしまい

以上です。

意外と簡単に物理演算をできることが伝われば嬉しいです。

Discussion