Skip to content

Commit b022515

Browse files
authored
タイル情報を表示するstateを追加した (#360)
ゲームプレイ上はもちろん、デバッグにも便利なので作成した
2 parents da56abb + 7d640a7 commit b022515

33 files changed

+655
-426
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
<img src="./vrtimages/DebugMenu.png" width="50%" /><img src="./vrtimages/ShopMenu.png" width="50%" />
1919

20+
<img src="./vrtimages/LookAround.png" width="50%" />
21+
2022
各画像は全自動でキャプチャされる。最新なことをCIで保証している。
2123

2224
## キーボード操作

internal/cmd/screenshot.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ func runScreenshot(_ context.Context, cmd *cli.Command) error {
3939
Depth: 1,
4040
BuilderType: mapplanner.PlannerTypeSmallRoom,
4141
})
42-
case gs.FieldInfoState{}.String():
42+
case gs.LookAroundState{}.String():
4343
return vrt.RunTestGame(mode, &gs.DungeonState{
4444
Depth: 1,
4545
BuilderType: mapplanner.PlannerTypeSmallRoom,
46-
}, &gs.FieldInfoState{})
46+
}, &gs.LookAroundState{})
4747
case gs.EquipMenuState{}.String():
4848
return vrt.RunTestGame(mode, townStateFactory(), &gs.EquipMenuState{})
4949
case "GameOver":

internal/consts/coord.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package consts
2+
3+
// Numeric は座標で使用可能な数値型を定義する
4+
type Numeric interface {
5+
~int | ~float64
6+
}
7+
8+
// Coord は2次元座標を表すジェネリック型
9+
type Coord[T Numeric] struct {
10+
X T
11+
Y T
12+
}

internal/engine/states/lib.go

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,10 @@ type ActionHandler[T any] interface {
6060
// StateFactory はステートを作成するファクトリー関数の型
6161
type StateFactory[T any] func() State[T]
6262

63-
// DrawHook はstate描画時のフック関数
64-
// stateIndex: 現在描画したstateのインデックス
65-
// stateCount: 現在のstateの総数
66-
type DrawHook[T any] func(
67-
stateIndex int,
68-
stateCount int,
69-
world T,
70-
screen *ebiten.Image,
71-
) error
72-
7363
// StateMachine はジェネリックな状態スタックを管理する
7464
type StateMachine[T any] struct {
7565
states []State[T]
7666
lastTransition Transition[T]
77-
AfterDrawHook DrawHook[T]
7867
}
7968

8069
// Init は新しいステートマシンを初期化する
@@ -133,20 +122,11 @@ func (sm *StateMachine[T]) Update(world T) error {
133122

134123
// Draw は画面を描画する
135124
func (sm *StateMachine[T]) Draw(world T, screen *ebiten.Image) error {
136-
stateCount := len(sm.states)
137-
for i, state := range sm.states {
125+
for _, state := range sm.states {
138126
if err := state.Draw(world, screen); err != nil {
139127
return err
140128
}
141-
142-
// 各state描画後にフックを呼び出し
143-
if sm.AfterDrawHook != nil {
144-
if err := sm.AfterDrawHook(i, stateCount, world, screen); err != nil {
145-
return err
146-
}
147-
}
148129
}
149-
150130
return nil
151131
}
152132

@@ -276,7 +256,7 @@ func (sm *StateMachine[T]) quit(world T) error {
276256
return nil
277257
}
278258

279-
// GetStates はステートスタックのコピーを返す(テスト用)
259+
// GetStates はステートスタックのコピーを返す
280260
func (sm *StateMachine[T]) GetStates() []State[T] {
281261
states := make([]State[T], len(sm.states))
282262
copy(states, sm.states)

internal/maingame/game.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/kijimaD/ruins/internal/loader"
1919
gr "github.com/kijimaD/ruins/internal/resources"
2020
"github.com/kijimaD/ruins/internal/screeneffect"
21+
"github.com/kijimaD/ruins/internal/states"
2122
gs "github.com/kijimaD/ruins/internal/systems"
2223
w "github.com/kijimaD/ruins/internal/world"
2324
)
@@ -31,9 +32,6 @@ type MainGame struct {
3132

3233
// NewMainGame はMainGameを初期化する
3334
func NewMainGame(world w.World, stateMachine es.StateMachine[w.World]) (*MainGame, error) {
34-
// オーバーレイ描画フックを設定
35-
stateMachine.AfterDrawHook = afterDrawHook
36-
3735
retroFilter, err := screeneffect.NewRetroFilter()
3836
if err != nil {
3937
return nil, fmt.Errorf("レトロフィルタの初期化に失敗: %w", err)
@@ -73,6 +71,7 @@ func (game *MainGame) Update() error {
7371
}
7472

7573
// Draw はゲームの描画処理を行う
74+
// interface method だからシグネチャは変更できない
7675
func (game *MainGame) Draw(screen *ebiten.Image) {
7776
bounds := screen.Bounds()
7877
offscreen := game.screenPipeline.Begin(bounds.Dx(), bounds.Dy())
@@ -83,8 +82,20 @@ func (game *MainGame) Draw(screen *ebiten.Image) {
8382
target = screen
8483
}
8584

86-
if err := game.StateMachine.Draw(game.World, target); err != nil {
87-
log.Fatal(err)
85+
// stateを描画し、必要に応じてブラーオーバーレイを適用する
86+
stateList := game.StateMachine.GetStates()
87+
for i, state := range stateList {
88+
if err := state.Draw(game.World, target); err != nil {
89+
log.Fatal(err)
90+
}
91+
92+
// 複数stateがあるとき、最初のstate描画後にブラーを適用する
93+
if i == 0 && len(stateList) > 1 {
94+
topState := stateList[len(stateList)-1]
95+
if cfg, ok := topState.(states.Configurable); !ok || cfg.StateConfig().BlurBackground {
96+
applyBlurOverlay(target, state)
97+
}
98+
}
8899
}
89100

90101
if game.World.Config.ShowMonitor {

internal/maingame/hooks.go

Lines changed: 49 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,71 @@ import (
44
"image/color"
55

66
"github.com/hajimehoshi/ebiten/v2"
7-
"github.com/hajimehoshi/ebiten/v2/vector"
7+
es "github.com/kijimaD/ruins/internal/engine/states"
88
w "github.com/kijimaD/ruins/internal/world"
99
)
1010

11-
// ブラー画像と黒背景のキャッシュ
11+
// ブラー画像と暗転オーバーレイのキャッシュ
1212
var (
13-
cachedBlurImage *ebiten.Image
14-
cachedBlackBackground *ebiten.Image
15-
cachedStateCount int
13+
cachedBlurImage *ebiten.Image
14+
cachedDarkOverlay *ebiten.Image
15+
cachedBaseState es.State[w.World]
1616
)
1717

18-
// afterDrawHook は各stateのDraw完了後に呼ばれるフック関数
19-
func afterDrawHook(stateIndex, stateCount int, _ w.World, screen *ebiten.Image) error {
20-
if cachedStateCount != stateCount {
21-
// キャッシュをクリア
18+
// applyBlurOverlay は画面にブラー効果を適用する
19+
// baseStateが変わるとキャッシュを再生成する
20+
func applyBlurOverlay(screen *ebiten.Image, baseState es.State[w.World]) {
21+
// 下層stateが変わったらキャッシュをクリア
22+
if cachedBaseState != baseState {
2223
cachedBlurImage = nil
23-
cachedStateCount = stateCount
24+
cachedBaseState = baseState
2425
}
2526

26-
// stateが複数あるとき初回のstate描画後に1回だけブラー効果を適用する
27-
if stateCount > 1 && stateIndex == 0 {
28-
bounds := screen.Bounds()
27+
bounds := screen.Bounds()
2928

30-
// キャッシュがない場合のみブラー処理を実行
31-
if cachedBlurImage == nil {
32-
// 現在の画面をコピー
33-
src := ebiten.NewImage(bounds.Dx(), bounds.Dy())
34-
src.DrawImage(screen, nil)
29+
// キャッシュがない場合のみブラー画像を生成する
30+
if cachedBlurImage == nil {
31+
w, h := bounds.Dx(), bounds.Dy()
3532

36-
// ブラー効果を2パス(水平→垂直)で適用
37-
// 水平ブラー
38-
const blurRadius = 4
39-
tmp := ebiten.NewImage(bounds.Dx(), bounds.Dy())
40-
for x := -blurRadius; x <= blurRadius; x++ {
41-
op := &ebiten.DrawImageOptions{}
42-
op.GeoM.Translate(float64(x), 0)
43-
op.ColorScale.ScaleAlpha(1.0 / float32(blurRadius*2+1))
44-
op.Blend = ebiten.BlendSourceOver
45-
tmp.DrawImage(src, op)
46-
}
33+
// 現在の画面をコピーする
34+
src := ebiten.NewImage(w, h)
35+
op := &ebiten.DrawImageOptions{}
36+
op.GeoM.Translate(float64(-bounds.Min.X), float64(-bounds.Min.Y))
37+
src.DrawImage(screen, op)
4738

48-
// 垂直ブラー
49-
cachedBlurImage = ebiten.NewImage(bounds.Dx(), bounds.Dy())
50-
for y := -blurRadius; y <= blurRadius; y++ {
51-
op := &ebiten.DrawImageOptions{}
52-
op.GeoM.Translate(0, float64(y))
53-
op.ColorScale.ScaleAlpha(1.0 / float32(blurRadius*2+1))
54-
op.Blend = ebiten.BlendSourceOver
55-
cachedBlurImage.DrawImage(tmp, op)
56-
}
57-
}
39+
// ブラー効果を2パス(水平→垂直)で適用する
40+
const blurRadius = 4
41+
kernel := blurRadius*2 + 1
5842

59-
// 黒い背景画像をキャッシュから取得または生成
60-
if cachedBlackBackground == nil {
61-
cachedBlackBackground = ebiten.NewImage(bounds.Dx(), bounds.Dy())
62-
width := float32(bounds.Dx())
63-
height := float32(bounds.Dy())
64-
vector.FillRect(cachedBlackBackground, 0, 0, width, height, color.RGBA{0, 0, 0, 255}, false)
43+
// 水平ブラー
44+
tmp := ebiten.NewImage(bounds.Dx(), bounds.Dy())
45+
for x := -blurRadius; x <= blurRadius; x++ {
46+
op := &ebiten.DrawImageOptions{}
47+
op.GeoM.Translate(float64(x), 0)
48+
op.ColorScale.Scale(1.0/float32(kernel), 1.0/float32(kernel), 1.0/float32(kernel), 1.0/float32(kernel))
49+
op.Blend = ebiten.BlendLighter
50+
tmp.DrawImage(src, op)
6551
}
6652

67-
screen.Clear()
68-
69-
// 黒背景を描画
70-
screen.DrawImage(cachedBlackBackground, nil)
53+
// 垂直ブラー
54+
cachedBlurImage = ebiten.NewImage(bounds.Dx(), bounds.Dy())
55+
for y := -blurRadius; y <= blurRadius; y++ {
56+
op := &ebiten.DrawImageOptions{}
57+
op.GeoM.Translate(0, float64(y))
58+
op.ColorScale.Scale(1.0/float32(kernel), 1.0/float32(kernel), 1.0/float32(kernel), 1.0/float32(kernel))
59+
op.Blend = ebiten.BlendLighter
60+
cachedBlurImage.DrawImage(tmp, op)
61+
}
62+
}
7163

72-
// その上にブラー画像を描画
73-
screen.DrawImage(cachedBlurImage, nil)
64+
// 黒い半透明オーバーレイをキャッシュから取得または生成する
65+
if cachedDarkOverlay == nil {
66+
cachedDarkOverlay = ebiten.NewImage(bounds.Dx(), bounds.Dy())
67+
cachedDarkOverlay.Fill(color.RGBA{0, 0, 0, 48})
7468
}
7569

76-
return nil
70+
drawOp := &ebiten.DrawImageOptions{}
71+
drawOp.GeoM.Translate(float64(bounds.Min.X), float64(bounds.Min.Y))
72+
screen.DrawImage(cachedBlurImage, drawOp)
73+
screen.DrawImage(cachedDarkOverlay, drawOp)
7774
}

internal/mapplanner/hostile_npc_planner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const (
1919

2020
// NPCSpec はNPC配置仕様を表す
2121
type NPCSpec struct {
22-
Coord
22+
consts.Coord[int]
2323
Name string // NPCタイプ
2424
}
2525

@@ -78,7 +78,7 @@ func (n *HostileNPCPlanner) PlanMeta(planData *MetaPlan) error {
7878
}
7979

8080
planData.NPCs = append(planData.NPCs, NPCSpec{
81-
Coord: Coord{X: int(tx), Y: int(ty)},
81+
Coord: consts.Coord[int]{X: int(tx), Y: int(ty)},
8282
Name: enemyName,
8383
})
8484

internal/mapplanner/item_planner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121

2222
// ItemSpec はアイテム配置仕様を表す
2323
type ItemSpec struct {
24-
Coord
24+
consts.Coord[int]
2525
Name string // アイテム名
2626
}
2727

@@ -100,7 +100,7 @@ func (i *ItemPlanner) addItem(planData *MetaPlan, itemName string) error {
100100

101101
// MetaPlanにアイテムを追加
102102
planData.Items = append(planData.Items, ItemSpec{
103-
Coord: Coord{X: int(x), Y: int(y)},
103+
Coord: consts.Coord[int]{X: int(x), Y: int(y)},
104104
Name: itemName,
105105
})
106106

internal/mapplanner/lib.go

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,9 @@ import (
1515
w "github.com/kijimaD/ruins/internal/world"
1616
)
1717

18-
// Coord はタイル座標を表す
19-
type Coord struct {
20-
X int
21-
Y int
22-
}
23-
2418
// PropsSpec はProps配置仕様を表す
2519
type PropsSpec struct {
26-
Coord
20+
consts.Coord[int]
2721
Name string // Prop名
2822
}
2923

@@ -43,9 +37,9 @@ type MetaPlan struct {
4337
// 通行可能かを判定するための情報を保持している必要がある
4438
Tiles []raw.TileRaw
4539
// NextPortals は次の階へ進むポータルリスト
46-
NextPortals []Coord
40+
NextPortals []consts.Coord[int]
4741
// EscapePortals は脱出用ポータルリスト
48-
EscapePortals []Coord
42+
EscapePortals []consts.Coord[int]
4943
// NPCs は配置予定のNPCリスト
5044
NPCs []NPCSpec
5145
// Items は配置予定のアイテムリスト
@@ -298,8 +292,8 @@ func NewPlannerChain(width consts.Tile, height consts.Tile, seed uint64) *Planne
298292
Rooms: []gc.Rect{},
299293
Corridors: [][]resources.TileIdx{},
300294
RNG: rand.New(rand.NewPCG(seed, seed+1)),
301-
NextPortals: []Coord{},
302-
EscapePortals: []Coord{},
295+
NextPortals: []consts.Coord[int]{},
296+
EscapePortals: []consts.Coord[int]{},
303297
NPCs: []NPCSpec{},
304298
Items: []ItemSpec{},
305299
Props: []PropsSpec{},
@@ -513,10 +507,10 @@ func (bm *MetaPlan) GetTile(name string) raw.TileRaw {
513507

514508
// GetPlayerStartPosition はプレイヤーの開始位置を取得する
515509
// ポータルへの到達性も確認し、到達可能な位置を返す
516-
func (bm *MetaPlan) GetPlayerStartPosition() (Coord, error) {
510+
func (bm *MetaPlan) GetPlayerStartPosition() (consts.Coord[int], error) {
517511
// SpawnPointsが設定されていればそれを使用(テンプレートマップ用)
518512
if len(bm.SpawnPoints) > 0 {
519-
return Coord{X: bm.SpawnPoints[0].X, Y: bm.SpawnPoints[0].Y}, nil
513+
return consts.Coord[int]{X: bm.SpawnPoints[0].X, Y: bm.SpawnPoints[0].Y}, nil
520514
}
521515

522516
// プロシージャルマップ用: 自動的に歩行可能でポータルに到達可能な位置を探す
@@ -527,12 +521,12 @@ func (bm *MetaPlan) GetPlayerStartPosition() (Coord, error) {
527521
pf := NewPathFinder(bm)
528522

529523
// 候補位置を試す(中央から外側へ)
530-
attempts := []Coord{
531-
{width / 2, height / 2}, // 中央
532-
{width / 4, height / 4}, // 左上寄り
533-
{3 * width / 4, height / 4}, // 右上寄り
534-
{width / 4, 3 * height / 4}, // 左下寄り
535-
{3 * width / 4, 3 * height / 4}, // 右下寄り
524+
attempts := []consts.Coord[int]{
525+
{X: width / 2, Y: height / 2}, // 中央
526+
{X: width / 4, Y: height / 4}, // 左上寄り
527+
{X: 3 * width / 4, Y: height / 4}, // 右上寄り
528+
{X: width / 4, Y: 3 * height / 4}, // 左下寄り
529+
{X: 3 * width / 4, Y: 3 * height / 4}, // 右下寄り
536530
}
537531

538532
for _, pos := range attempts {
@@ -547,12 +541,12 @@ func (bm *MetaPlan) GetPlayerStartPosition() (Coord, error) {
547541
i := resources.TileIdx(_i)
548542
x, y := bm.Level.XYTileCoord(i)
549543
if bm.isValidSpawnPosition(pf, int(x), int(y)) {
550-
return Coord{X: int(x), Y: int(y)}, nil
544+
return consts.Coord[int]{X: int(x), Y: int(y)}, nil
551545
}
552546
}
553547
}
554548

555-
return Coord{}, fmt.Errorf("ポータルに到達可能な歩行可能タイルが見つかりません")
549+
return consts.Coord[int]{}, fmt.Errorf("ポータルに到達可能な歩行可能タイルが見つかりません")
556550
}
557551

558552
// isValidSpawnPosition は指定位置がスポーン可能かつポータルに到達可能かを判定する

0 commit comments

Comments
 (0)