Flutter游戏开发框架插件game_scaffold的使用

Flutter游戏开发框架插件game_scaffold的使用

game_scaffold 是一个用于游戏开发的 Flutter 扩展插件,基于 game_scaffold_dart。它提供了一套完整的解决方案来帮助开发者快速搭建和管理游戏逻辑。

使用示例

示例代码

import 'package:flutter/material.dart';
import 'package:game_scaffold/game_scaffold.dart';
import 'package:game_scaffold_games/games.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';

void main() {
  TicTacToeGame.register();

  Logger.root.clearListeners();
  Logger.root.level = Level.FINE;
  Logger.root.onRecord.listen((record) =>
      // 忽略:避免打印
      print('[${record.level}] ${record.loggerName}: ${record.message}'));
  runApp(ProviderScope(
    overrides: [
      GameProviders.clientType
          .overrideWithValue(StateController(OnDeviceClient)),
    ],
    child: const TicTacToeApp(),
  ));
}

class TicTacToeApp extends StatelessWidget {
  const TicTacToeApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData.dark().copyWith(
          primaryColor: Colors.blueGrey,
        ),
        home: const TicTacToeWidget(),
      );
}

class TicTacToeWidget extends StatelessWidget {
  const TicTacToeWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) => MaterialApp(
          home: Scaffold(
        body: Row(children: [
          Expanded(
            child: ProviderScope(
              overrides: [GameProviders.playerID.overrideWithValue(P1)],
              child: const Player(),
            ),
          ),
          Container(width: 10, color: Colors.black),
          Expanded(
            child: ProviderScope(
              overrides: [GameProviders.playerID.overrideWithValue(P2)],
              child: const Player(),
            ),
          ),
        ]),
      ));
}

class Player extends HookConsumerWidget {
  const Player({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) => const GameNavigator(
        connected: CreateOrJoinWidget(),
        lobby: LobbyWidget(),
        game: GameWidget(),
      );
}

class CreateOrJoinWidget extends HookConsumerWidget {
  const CreateOrJoinWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final playerID = ref.watch(GameProviders.playerID);
    final allGames = ref.watch(GameProviders.allGames);

    return Scaffold(
      body: Center(
        child: Column(
          children: [
            const SizedBox(height: 40),
            Text('Player $playerID'),
            const SizedBox(height: 20),
            if (playerID == P1)
              ElevatedButton(
                key: Key('Create Game Button $playerID'),
                onPressed: () async {
                  ref.read(GameProviders.config.notifier).state =
                      const GameConfig(
                    adminID: P1,
                    customNames: false,
                    gameType: 'tictactoe',
                    rounds: 2,
                    maxPlayers: 2,
                  );
                  await GameProviders.createGame.refresh(ref);
                  await GameProviders.joinGame.refresh(ref);
                },
                child: const Text('Create Game'),
              ),
            if (playerID == P2) ...[
              SizedBox(
                width: 200,
                height: 30,
                child: TextField(
                  textAlignVertical: TextAlignVertical.center,
                  decoration:
                      const InputDecoration(hintText: 'Enter Game Code'),
                  onChanged: (text) =>
                      ref.read(GameProviders.code.notifier).state = text,
                ),
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  GameProviders.joinGame.refresh(ref);
                },
                child: const Text('Join Game'),
              )
            ],
            if (allGames.value != null)
              for (final info in allGames.value!)
                ElevatedButton(
                  onPressed: () async {
                    ref.read(GameProviders.code.notifier).state = info.gameId;
                    await GameProviders.joinGame.refresh(ref);
                  },
                  child: Text(
                      'Started Game: ${info.gameId}, Players: ${info.players}'),
                ),
          ],
        ),
      ),
    );
  }
}

class LobbyWidget extends HookConsumerWidget {
  const LobbyWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final lobby = ref.watch(GameProviders.lobby);
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(children: [
          const SizedBox(height: 40),
          const Text('Lobby'),
          Text('${lobby.value}'),
        ]),
      ),
    );
  }
}

class GameWidget extends HookConsumerWidget {
  const GameWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final gameState = ref.watch(GameProviders.game);
    final gameStatus = ref.watch(GameProviders.status);
    final playerID = ref.watch(GameProviders.playerID);
    ref.listen<AsyncValue<GameError>>(GameProviders.error, (prevError, error) {
      if (error != prevError) {
        showDialog(
          context: context,
          builder: (c) => Dialog(
            backgroundColor: Colors.white,
            child: Text(error.toString()),
          ),
        );
      }
    });
    return gameState.when(
      error: (e, st) => Text('$e, $st'),
      loading: () => const Center(child: CircularProgressIndicator()),
      data: (g) => Scaffold(
        appBar: AppBar(),
        body: Center(
          child: ListView(
            children: [
              Text('$gameState'),
              const SizedBox(height: 20),
              for (final r in [0, 1, 2])
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    for (final c in [0, 1, 2])
                      GestureDetector(
                        key: Key('$playerID square $r $c'),
                        onTap: () async {
                          await GameProviders.sendEvent(
                            TicTacToeGameEvent(playerID, r * 3 + c).asGameEvent,
                          ).refresh(ref);
                        },
                        child: ColoredBox(
                          color: Colors.black,
                          child: Container(
                            width: 20,
                            height: 20,
                            color: Colors.white,
                            margin: const EdgeInsets.all(1),
                            child: Center(
                              child: Text(
                                (g as TicTacToeGame)
                                    .board
                                    .xOrO(playerID, r * 3 + c),
                              ),
                            ),
                          ),
                        ),
                      ),
                  ],
                ),
              if (gameStatus == GameStatus.BetweenRounds && !g.readyPlayers.contains(playerID)) ...[
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () async {
                    await GameProviders.newRound.refresh(ref);
                  },
                  child: const Text('Next Round'),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

extension TextX on List<String?> {
  String xOrO(String playerID, int location) {
    if (this[location] == null) {
      return '';
    } else if (this[location] == P1) {
      return 'X';
    } else {
      return 'O';
    }
  }
}

示例说明

  1. 导入库

    import 'package:flutter/material.dart';
    import 'package:game_scaffold/game_scaffold.dart';
    import 'package:game_scaffold_games/games.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    import 'package:logging/logging.dart';
    
  2. 主函数

    void main() {
      TicTacToeGame.register();
    
      Logger.root.clearListeners();
      Logger.root.level = Level.FINE;
      Logger.root.onRecord.listen((record) =>
          // 忽略:避免打印
          print('[${record.level}] ${record.loggerName}: ${record.message}'));
    
      runApp(ProviderScope(
        overrides: [
          GameProviders.clientType
              .overrideWithValue(StateController(OnDeviceClient)),
        ],
        child: const TicTacToeApp(),
      ));
    }
    
  3. 创建应用

    class TicTacToeApp extends StatelessWidget {
      const TicTacToeApp({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) => MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData.dark().copyWith(
              primaryColor: Colors.blueGrey,
            ),
            home: const TicTacToeWidget(),
          );
    }
    
  4. 游戏布局

    class TicTacToeWidget extends StatelessWidget {
      const TicTacToeWidget({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) => MaterialApp(
              home: Scaffold(
            body: Row(children: [
              Expanded(
                child: ProviderScope(
                  overrides: [GameProviders.playerID.overrideWithValue(P1)],
                  child: const Player(),
                ),
              ),
              Container(width: 10, color: Colors.black),
              Expanded(
                child: ProviderScope(
                  overrides: [GameProviders.playerID.overrideWithValue(P2)],
                  child: const Player(),
                ),
              ),
            ]),
          ));
    }
    
  5. 玩家组件

    class Player extends HookConsumerWidget {
      const Player({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) => const GameNavigator(
            connected: CreateOrJoinWidget(),
            lobby: LobbyWidget(),
            game: GameWidget(),
          );
    }
    
  6. 创建或加入游戏

    class CreateOrJoinWidget extends HookConsumerWidget {
      const CreateOrJoinWidget({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final playerID = ref.watch(GameProviders.playerID);
        final allGames = ref.watch(GameProviders.allGames);
    
        return Scaffold(
          body: Center(
            child: Column(
              children: [
                const SizedBox(height: 40),
                Text('Player $playerID'),
                const SizedBox(height: 20),
                if (playerID == P1)
                  ElevatedButton(
                    key: Key('Create Game Button $playerID'),
                    onPressed: () async {
                      ref.read(GameProviders.config.notifier).state =
                          const GameConfig(
                        adminID: P1,
                        customNames: false,
                        gameType: 'tictactoe',
                        rounds: 2,
                        maxPlayers: 2,
                      );
                      await GameProviders.createGame.refresh(ref);
                      await GameProviders.joinGame.refresh(ref);
                    },
                    child: const Text('Create Game'),
                  ),
                if (playerID == P2) ...[
                  SizedBox(
                    width: 200,
                    height: 30,
                    child: TextField(
                      textAlignVertical: TextAlignVertical.center,
                      decoration:
                          const InputDecoration(hintText: 'Enter Game Code'),
                      onChanged: (text) =>
                          ref.read(GameProviders.code.notifier).state = text,
                    ),
                  ),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      GameProviders.joinGame.refresh(ref);
                    },
                    child: const Text('Join Game'),
                  )
                ],
                if (allGames.value != null)
                  for (final info in allGames.value!)
                    ElevatedButton(
                      onPressed: () async {
                        ref.read(GameProviders.code.notifier).state = info.gameId;
                        await GameProviders.joinGame.refresh(ref);
                      },
                      child: Text(
                          'Started Game: ${info.gameId}, Players: ${info.players}'),
                    ),
              ],
            ),
          ),
        );
      }
    }
    
  7. 大厅组件

    class LobbyWidget extends HookConsumerWidget {
      const LobbyWidget({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final lobby = ref.watch(GameProviders.lobby);
        return Scaffold(
          appBar: AppBar(),
          body: Center(
            child: Column(children: [
              const SizedBox(height: 40),
              const Text('Lobby'),
              Text('${lobby.value}'),
            ]),
          ),
        );
      }
    }
    
  8. 游戏组件

    class GameWidget extends HookConsumerWidget {
      const GameWidget({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final gameState = ref.watch(GameProviders.game);
        final gameStatus = ref.watch(GameProviders.status);
        final playerID = ref.watch(GameProviders.playerID);
        ref.listen<AsyncValue<GameError>>(GameProviders.error, (prevError, error) {
          if (error != prevError) {
            showDialog(
              context: context,
              builder: (c) => Dialog(
                backgroundColor: Colors.white,
                child: Text(error.toString()),
              ),
            );
          }
        });
        return gameState.when(
          error: (e, st) => Text('$e, $st'),
          loading: () => const Center(child: CircularProgressIndicator()),
          data: (g) => Scaffold(
            appBar: AppBar(),
            body: Center(
              child: ListView(
                children: [
                  Text('$gameState'),
                  const SizedBox(height: 20),
                  for (final r in [0, 1, 2])
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        for (final c in [0, 1, 2])
                          GestureDetector(
                            key: Key('$playerID square $r $c'),
                            onTap: () async {
                              await GameProviders.sendEvent(
                                TicTacToeGameEvent(playerID, r * 3 + c).asGameEvent,
                              ).refresh(ref);
                            },
                            child: ColoredBox(
                              color: Colors.black,
                              child: Container(
                                width: 20,
                                height: 20,
                                color: Colors.white,
                                margin: const EdgeInsets.all(1),
                                child: Center(
                                  child: Text(
                                    (g as TicTacToeGame)
                                        .board
                                        .xOrO(playerID, r * 3 + c),
                                  ),
                                ),
                              ),
                            ),
                          ),
                      ],
                    ),
                  if (gameStatus == GameStatus.BetweenRounds && !g.readyPlayers.contains(playerID)) ...[
                    const SizedBox(height: 20),
                    ElevatedButton(
                      onPressed: () async {
                        await GameProviders.newRound.refresh(ref);
                      },
                      child: const Text('Next Round'),
                    ),
                  ],
                ],
              ),
            ),
          ),
        );
      }
    }
    
  9. 扩展方法

    extension TextX on List<String?> {
      String xOrO(String playerID, int location) {
        if (this[location] == null) {
          return '';
        } else if (this[location] == P1) {
          return 'X';
        } else {
          return 'O';
        }
      }
    }
    

更多关于Flutter游戏开发框架插件game_scaffold的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter游戏开发框架插件game_scaffold的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


game_scaffold 是一个为 Flutter 游戏开发提供的基础框架插件,它旨在简化游戏开发中的一些常见任务,例如管理游戏循环、处理输入、渲染等。通过使用 game_scaffold,开发者可以更专注于游戏逻辑的实现,而不必从头开始搭建基础框架。

安装 game_scaffold

首先,你需要在 pubspec.yaml 文件中添加 game_scaffold 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  game_scaffold: ^0.1.0  # 请使用最新版本

然后运行 flutter pub get 来安装依赖。

基本使用

game_scaffold 提供了一个 GameScaffold 类,你可以将其作为游戏的主框架。以下是一个简单的使用示例:

import 'package:flutter/material.dart';
import 'package:game_scaffold/game_scaffold.dart';

void main() {
  runApp(MyGame());
}

class MyGame extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GameScaffold(
        game: MyGameLogic(),
      ),
    );
  }
}

class MyGameLogic extends Game {
  @override
  void update(double delta) {
    // 更新游戏逻辑
  }

  @override
  void render(Canvas canvas) {
    // 渲染游戏内容
  }
}

GameScaffold 的主要功能

  1. 游戏循环管理GameScaffold 会自动管理游戏循环,调用 updaterender 方法。你只需要在 update 方法中处理游戏逻辑,在 render 方法中绘制游戏内容。

  2. 输入处理GameScaffold 提供了对触摸输入的支持。你可以通过重写 onTapDownonTapUp 等方法来处理用户的触摸事件。

  3. 屏幕适配GameScaffold 会自动处理不同屏幕尺寸的适配问题,确保游戏在不同设备上都能正常显示。

示例:处理触摸事件

class MyGameLogic extends Game {
  Offset? tapPosition;

  @override
  void update(double delta) {
    // 更新游戏逻辑
  }

  @override
  void render(Canvas canvas) {
    if (tapPosition != null) {
      final paint = Paint()..color = Colors.red;
      canvas.drawCircle(tapPosition!, 20, paint);
    }
  }

  @override
  void onTapDown(TapDownDetails details) {
    tapPosition = details.localPosition;
  }

  @override
  void onTapUp(TapUpDetails details) {
    tapPosition = null;
  }
}

在这个示例中,当用户触摸屏幕时,游戏会在触摸位置绘制一个红色的圆圈。当用户松开手指时,圆圈会消失。

自定义游戏循环

如果你需要自定义游戏循环的频率,可以通过 GameScaffoldupdateInterval 参数来设置:

GameScaffold(
  game: MyGameLogic(),
  updateInterval: Duration(milliseconds: 16), // 大约 60 FPS
);
回到顶部