flutter3d_game 0.7.0 copy "flutter3d_game: ^0.7.0" to clipboard
flutter3d_game: ^0.7.0 copied to clipboard

PlatformmacOS

What a game on flutter3d adds to an application: touch, keyboard, mouse and gamepad input, the run being played, the screens that are not the game, and the actors and fixtures a simulation moves, drawn.

example/lib/main.dart

/// A level you can walk around, which is what a new project starts as.
///
///     flutter run -d macos
///
/// **This is a seed, not a game.** It reads the level in `assets/levels/`,
/// builds it, and puts a body in it that walks, looks and jumps. What it
/// deliberately does not do is anything a *genre* does: no weapons, no
/// monsters, no coins, no doors that open, no score, no menu, no saving. Those
/// live in `flutter3d_game_shooter` and `flutter3d_game_platformer`, and wiring one
/// up is the next thing to do here — the three games in this repository are the
/// worked examples, and each of them keeps that wiring in its own
/// `lib/src/staging.dart`.
///
/// It is a real application in this repository as well as a template, so that
/// it is analysed and compiled by CI. **A `main.dart` that is only ever a
/// string in a scaffolder is one that stops compiling six months later and
/// nobody finds out until somebody creates a project.**
library;

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart' hide Material;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter3d/flutter3d.dart' hide Material;
import 'package:flutter3d_app/flutter3d_app.dart';
import 'package:flutter3d_game/flutter3d_game.dart';
import 'package:flutter3d_sim/flutter3d_sim.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:vector_math/vector_math.dart' hide Colors;

/// The level this project opens with.
const String kLevel = String.fromEnvironment(
  'level',
  defaultValue: 'assets/levels/first.json',
);

void main() => runApp(const TemplateApp());

class TemplateApp extends StatelessWidget {
  const TemplateApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'A level',
    debugShowCheckedModeBanner: false,
    home: const LevelScreen(),
  );
}

/// What the level is doing, as far as the screen is concerned.
///
/// Screen state, and only that — this seed has no restart, no next level and
/// no save to model, so a plain `Cubit` over three states is enough on its own.
/// `RunSession`, in `flutter3d_game`, is for once one of those shows up; see
/// its doc comment for why [LevelReady] below is still safe to hold the scene
/// and the body in even then — they do not change sixty times a second, only
/// what is inside them does, and that is read by the render loop directly
/// rather than republished as a new state on every frame.
sealed class LevelState {
  const LevelState();
}

/// Before the level is up.
final class LevelLoading extends LevelState {
  const LevelLoading();
}

/// The level is built: a scene to draw, and a walk to go round it with.
final class LevelReady extends LevelState {
  const LevelReady(this.scene, this.walk);

  final Scene scene;
  final LevelWalk walk;
}

/// The level would not load, and why.
///
/// **A level that will not read used to be a black screen for ever**: the load
/// caught its own throw and printed it, which is a line in a console nobody
/// playing the game can see.
final class LevelFailed extends LevelState {
  const LevelFailed(this.error);

  final Object error;
}

/// Reads [kLevel] and builds everything it takes to walk around in it.
///
/// [device] is taken rather than opened in here, the same split
/// `flutter3d_demo_dungeon`'s `DungeonRun` makes and for the same reason: a
/// test can hand over a `CpuDevice` and drive the whole load without a window,
/// which is what `test/level_cubit_test.dart` does.
class LevelCubit extends Cubit<LevelState> {
  LevelCubit() : super(const LevelLoading());

  /// [world] is where the body collides; [camera] is added to the scene once
  /// it is built, so the widget never has to reach back in and do it. [asset]
  /// defaults to [kLevel] — overridable so a test can point at a level that is
  /// not there and see [LevelFailed] rather than a hang.
  Future<void> open(
    GraphicsDevice device, {
    required CollisionWorld world,
    required CameraNode camera,
    String asset = kLevel,
  }) async {
    try {
      // Read first, build second: the registry is made out of what the
      // document happens to name, which is not knowable before reading it.
      final level = Level.fromJson(
        jsonDecode(await rootBundle.loadString(asset)) as Map<String, Object?>,
      );
      final loaded = await LevelLoader().build(
        level,
        device: device,
        // Every type the document names is accepted and none is given a
        // meaning, until there is something to spawn them into.
        registry: openRegistryFor(level),
      );
      loaded.level.addTo(world);

      final spawn = LevelWalk.spawnIn(level);
      emit(
        LevelReady(
          loaded.scene..add(camera),
          LevelWalk(world: world, at: spawn.at, yaw: spawn.yaw),
        ),
      );
    } catch (error) {
      emit(LevelFailed(error));
    }
  }
}

class LevelScreen extends StatefulWidget {
  const LevelScreen({super.key});

  @override
  State<LevelScreen> createState() => _LevelScreenState();
}

class _LevelScreenState extends State<LevelScreen>
    with SingleTickerProviderStateMixin {
  Renderer? _renderer;

  /// The device failed to open, or the renderer failed to build on top of it.
  /// Kept apart from [LevelFailed]: that is a level's own document being
  /// wrong, this is the machine underneath being unable to draw at all, and
  /// the two want different words on screen.
  Object? _initError;

  final CollisionWorld _world = CollisionWorld();

  final CameraNode _camera = CameraNode(name: 'eye');
  late final RenderView _view = RenderView(
    camera: _camera,
    clearColor: Vector4(0.05, 0.05, 0.07, 1.0),
  );

  final InputState _input = InputState();
  late final DesktopInput _keys = DesktopInput(state: _input);
  final FocusNode _keyboard = FocusNode();

  Offset? _dragged;

  Ticker? _ticker;

  /// How long since the last frame, and how long since the first.
  final FrameClock _frames = FrameClock();

  /// Owned here rather than reached for through `BlocProvider.of`: nothing
  /// else in this seed needs to see it, and the game loop below reads its
  /// state directly, sixty times a second, which is no place for a lookup.
  final LevelCubit _level = LevelCubit();

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_onTick)..start();
    unawaited(_open());
  }

  Future<void> _open() async {
    final GraphicsDevice device;
    try {
      // Through `openDevice` rather than by naming a backend — see
      // `src/backend.dart`. It picks Impeller or WebGL for the build, and
      // falls back to the software rasteriser at run time when flutter_gpu
      // will not start, which is the difference between a window that draws
      // slowly and one that draws nothing.
      //
      // The size is what the software fallback would draw at; the two hardware
      // backends size themselves to the surface and ignore it.
      device = await openDevice(width: 1280, height: 720);
      if (!mounted) return;
      setState(() => _renderer = Renderer.create(device: device));
    } catch (error) {
      if (mounted) setState(() => _initError = error);
      return;
    }
    // The cubit handles its own failures from here — see [LevelFailed] —
    // so nothing thrown by a bad document reaches this `try` at all.
    await _level.open(device, world: _world, camera: _camera);
  }

  void _onTick(Duration _) {
    // The ticker's argument is the frame's scheduled time, not the present;
    // `FrameClock` says why the wall is measured instead.
    final dt = _frames.tick();

    final state = _level.state;
    if (state is! LevelReady) return;
    // A jump if one was pressed, a walk where the keys point turned by where
    // the head is, a run while sprint is held.
    state.walk.step(dt, _input);

    if (mounted) setState(() {});
  }

  /// Puts the camera at eye height, looking where the mouse has been dragged.
  void _place() {
    final state = _level.state;
    if (state is LevelReady) state.walk.placeCamera(_camera);
  }

  @override
  void dispose() {
    _ticker?.dispose();
    _keyboard.dispose();
    unawaited(_keys.dispose());
    unawaited(_level.close());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final initError = _initError;
    if (initError != null) return _didNotStart(initError);

    final renderer = _renderer;
    if (renderer == null) return _loading();

    return BlocProvider.value(
      value: _level,
      // Everything drawn from the state, including the one that is not playing
      // yet. Which way the spawn faces is the walk's own business now.
      child: BlocBuilder<LevelCubit, LevelState>(
        builder: (BuildContext context, LevelState state) => switch (state) {
          LevelFailed(:final error) => _didNotStart(error),
          LevelLoading() => _loading(),
          LevelReady(:final scene) => _game(renderer, scene),
        },
      ),
    );
  }

  Widget _didNotStart(Object error) => DidNotStart(
    error,
    background: const Color(0xFF14161A),
    foreground: const Color(0xFFFF8A80),
  );

  Widget _loading() => const Scaffold(
    backgroundColor: Color(0xFF14161A),
    body: Center(child: CircularProgressIndicator()),
  );

  Widget _game(Renderer renderer, Scene scene) => Scaffold(
    backgroundColor: const Color(0xFF14161A),
    body: Focus(
      focusNode: _keyboard,
      autofocus: true,
      onKeyEvent: (FocusNode node, KeyEvent event) =>
          _keys.handleKeyEvent(event),
      child: Listener(
        onPointerDown: (PointerDownEvent event) {
          _keyboard.requestFocus();
          _dragged = event.localPosition;
        },
        onPointerMove: (PointerMoveEvent event) {
          final from = _dragged;
          if (from == null) return;
          final by = event.localPosition - from;
          _dragged = event.localPosition;
          final state = _level.state;
          if (state is LevelReady) state.walk.look(by.dx, by.dy);
        },
        onPointerUp: (_) => _dragged = null,
        child: Stack(
          fit: StackFit.expand,
          children: <Widget>[
            SceneSurface(
              renderer: renderer,
              scene: scene,
              view: _view,
              settings: () => const RenderSettings(),
              onBeforeFrame: _place,
              presentFrame: presentFrame,
            ),
            const Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: ColoredBox(
                color: Color(0xCC0E1013),
                child: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                  child: Text(
                    'W A S D walk · shift runs · space jumps · drag to look',
                    style: TextStyle(color: Color(0xFF9AA4B2), fontSize: 12),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}
0
likes
150
points
403
downloads

Documentation

API reference

Publisher

verified publisherpleion.dev

Weekly Downloads

What a game on flutter3d adds to an application: touch, keyboard, mouse and gamepad input, the run being played, the screens that are not the game, and the actors and fixtures a simulation moves, drawn.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#game-development #game-engine #input #simulation

License

MIT (license)

Dependencies

flutter, flutter3d, flutter3d_app, flutter3d_audio, flutter3d_particles, flutter3d_sim, flutter_bloc, pad_input, pointer_lock, vector_math

More

Packages that depend on flutter3d_game