feat: Add code highlighter, game example and fix build

This commit is contained in:
Luan Nico
2025-08-23 15:44:25 -04:00
parent 2acb001eba
commit fe5b4f9ffa
19 changed files with 494 additions and 23 deletions
+1 -1
View File
@@ -34,4 +34,4 @@ jobs:
with:
node-version: 18
- run: npm install -g markdownlint-cli
- run: markdownlint . -c .markdownlint.yaml
- run: markdownlint . -p .markdownlintignore -c .markdownlint.yaml
+1
View File
@@ -0,0 +1 @@
**/macos/**
+20 -6
View File
@@ -1,15 +1,29 @@
# Flutter & Friends 2025 - flame_3d Workshop
This is a central hub for all materials related to @luanpotter and @wolfenrain's Flame 3D Workshop on Flutter & Friends 2025, including slides, templates and examples.
The goal of the workshop is to get everyone set up and running with a basis for creating 3D games or applications using the `flame_3d` package.
This is a central hub for all materials related to @luanpotter and @wolfenrain's Flame 3D Workshop
on Flutter & Friends 2025, including slides, templates and examples.
The goal of the workshop is to get everyone set up and running with a basis for creating 3D games or
applications using the `flame_3d` package.
## Requirements
* impeller, flutter gpu and especially flame_3d are highly experimental; join if you want to trailblaze, pave new paths, work and discover together. Do not join if you want production ready, out-of-box, stable and tested frameworks.
* currently, we only extensively tested flame_3d on macOS. Sadly, flutter_gpu does not work on Linux (or Windows) yet. We are highly anticipating the moment that the gods of Flutter bestown Linux support upon us. In the meantime, we are relegated to using macOS. We've heard that it should work on Android and iOS but have not tested it thoroughly.
* no prior knowledge about 3D rendering, how games or Flame work is required.
- impeller, flutter gpu and especially flame_3d are highly experimental; join if you want to
trailblaze, pave new paths, work and discover together. Do not join if you want production ready,
out-of-box, stable and tested frameworks.
- currently, we only extensively tested flame_3d on macOS. Sadly, flutter_gpu does not work on
Linux (or Windows) yet. We are highly anticipating the moment that the gods of Flutter bestow Linux
support upon us. In the meantime, we are relegated to using macOS. We've heard that it should work
on Android and iOS but have not tested it thoroughly.
- no prior knowledge about 3D rendering, how games or Flame work is required.
## Outcome
We will start from scratch and get some 3D objects into the world, a player-controlled character, camera and lighting. Then, depending on what each atendee wants to accomplish, we can cover textures, skeletal animations, and more.
We will start from scratch and get some 3D objects into the world, a player-controlled character,
camera and lighting. Then, depending on what each attendee wants to accomplish, we can cover
textures, skeletal animations, and more.
+25 -9
View File
@@ -1,16 +1,32 @@
# slides
A new Flutter project.
## Getting Started
An interactive slide deck for the Flame 3D Workshop.
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
## Setup
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Clone this monorepo to get started, or use `ignite` to generate your own Flame 3D project.
In order to run, you will need to follow
[the pre-requisites for setting up flame_3d](https://github.com/flame-engine/flame/tree/main/packages/flame_3d#prerequisites)
; notably:
- Run it on macOS;
- Enable Impeller by adding the following key to `/macos/Runner/Info.plist`:
```xml
<dict>
...
<key>FLTEnableImpeller</key>
<true/>
</dict>
```
- And then run with:
```bash
flutter run -d macos --enable-flutter-gpu
```
@@ -0,0 +1,65 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/palette.dart';
import 'package:flame_3d/components.dart';
import 'package:flame_3d/resources.dart';
class RoomBounds extends Component {
@override
FutureOr<void> onLoad() {
addAll([
// Floor
MeshComponent(
mesh: PlaneMesh(
size: Vector2(32, 32),
material: SpatialMaterial(
albedoTexture: ColorTexture(
BasicPalette.gray.color,
),
),
),
),
// Front wall
MeshComponent(
position: Vector3(16.5, 2.5, 0),
mesh: CuboidMesh(
size: Vector3(1, 5, 32),
material: SpatialMaterial(
albedoTexture: ColorTexture(
BasicPalette.yellow.color,
),
),
),
),
// Left wall
MeshComponent(
position: Vector3(0, 2.5, 16.5),
mesh: CuboidMesh(
size: Vector3(32, 5, 1),
material: SpatialMaterial(
albedoTexture: ColorTexture(
BasicPalette.blue.color,
),
),
),
),
// Right wall
MeshComponent(
position: Vector3(0, 2.5, -16.5),
mesh: CuboidMesh(
size: Vector3(32, 5, 1),
material: SpatialMaterial(
albedoTexture: ColorTexture(
BasicPalette.lime.color,
),
),
useFaceNormals: false,
),
),
]);
}
}
+105
View File
@@ -0,0 +1,105 @@
import 'dart:math';
import 'package:flame/components.dart' show HasGameReference, KeyboardHandler;
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flame_3d/components.dart';
import 'package:flame_3d/game.dart';
import 'package:flame_3d/resources.dart';
import 'package:flame_3d_workshop_slides/game/playground.dart';
import 'package:flame_3d_workshop_slides/game/util/keyboard_utils.dart';
import 'package:flutter/services.dart';
class Player extends MeshComponent
with HasGameReference<Playground>, KeyboardHandler {
final Vector2 _input = Vector2.zero();
bool isRunning = false;
double speedY = 0.0;
double _lookAngle = 0.0;
double get lookAngle => _lookAngle;
set lookAngle(double value) {
_lookAngle = value % tau;
transform.rotation.setAxisAngle(_up, value);
}
Vector3 get lookAt => Vector3(sin(_lookAngle), 0.0, cos(_lookAngle));
Player({required Vector3 position})
: super(
position: position,
mesh: CuboidMesh(
size: Vector3(1, 2, 1),
material: SpatialMaterial(
albedoTexture: ColorTexture(BasicPalette.yellow.color),
),
),
);
@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final shiftKeys = {
LogicalKeyboardKey.shift,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
};
isRunning = shiftKeys.any(keysPressed.contains);
final isDown = event is KeyDownEvent || event is KeyRepeatEvent;
if (isDown && event.logicalKey == LogicalKeyboardKey.space) {
jump();
return false;
}
return readArrowLikeKeysIntoVector2(event, keysPressed, _input);
}
@override
void update(double dt) {
super.update(dt);
_handleMovement(dt);
}
void reset() {
position.setFrom(Vector3(0, 1, 0));
lookAngle = 0.0;
_input.setZero();
}
void jump() {
if (position.y == _floorHeight) {
speedY = _jumpSpeed;
}
}
void _handleMovement(double dt) {
lookAngle += -_input.x * _rotationSpeed * dt;
final runningModifier = isRunning ? 2.5 : 1.0;
final movement = lookAt.scaled(
-_input.y * runningModifier * _walkingSpeed * dt,
);
position.add(movement);
if (speedY != 0 || position.y > _floorHeight) {
position.y += speedY * dt + 0.5 * _accY * dt * dt;
speedY += _accY * dt;
if (position.y < _floorHeight) {
position.y = _floorHeight;
speedY = 0;
}
} else {
position.y = _floorHeight;
speedY = 0;
}
}
static const double _rotationSpeed = 3.0;
static const double _walkingSpeed = 1.85;
static const double _floorHeight = 1.0;
static const double _jumpSpeed = 5.0;
static const double _accY = -9.81;
static final Vector3 _up = Vector3(0, 1, 0);
}
+25
View File
@@ -0,0 +1,25 @@
import 'dart:async';
import 'package:flame/events.dart';
import 'package:flame_3d/components.dart';
import 'package:flame_3d/game.dart';
import 'package:flame_3d_workshop_slides/game/components/room_bounds.dart';
import 'package:flame_3d_workshop_slides/game/player.dart';
class Playground extends FlameGame3D with HasKeyboardHandlerComponents {
late Player player;
@override
FutureOr<void> onLoad() async {
world.addAll([
RoomBounds(),
LightComponent.ambient(
intensity: 1.0,
),
player = Player(
position: Vector3(0, 1, 0),
),
]);
return super.onLoad();
}
}
@@ -0,0 +1,53 @@
import 'package:flame_3d/core.dart';
import 'package:flutter/services.dart';
// TODO(luan): add this as part of the input API
bool readArrowLikeKeysIntoVector2(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
Vector2 vector, {
LogicalKeyboardKey up = LogicalKeyboardKey.keyW,
LogicalKeyboardKey down = LogicalKeyboardKey.keyS,
LogicalKeyboardKey left = LogicalKeyboardKey.keyA,
LogicalKeyboardKey right = LogicalKeyboardKey.keyD,
}) {
final isDown = event is KeyDownEvent || event is KeyRepeatEvent;
if (event.logicalKey == up) {
if (isDown) {
vector.y = -1;
} else if (keysPressed.contains(down)) {
vector.y = 1;
} else {
vector.y = 0;
}
return false;
} else if (event.logicalKey == down) {
if (isDown) {
vector.y = 1;
} else if (keysPressed.contains(up)) {
vector.y = -1;
} else {
vector.y = 0;
}
return false;
} else if (event.logicalKey == left) {
if (isDown) {
vector.x = -1;
} else if (keysPressed.contains(right)) {
vector.x = 1;
} else {
vector.x = 0;
}
return false;
} else if (event.logicalKey == right) {
if (isDown) {
vector.x = 1;
} else if (keysPressed.contains(left)) {
vector.x = -1;
} else {
vector.x = 0;
}
return false;
}
return true;
}
+17 -4
View File
@@ -1,9 +1,12 @@
import 'package:flame_3d_workshop_slides/slides/slides.dart';
import 'package:flutter/material.dart' show ThemeMode;
import 'package:flutter/widgets.dart';
import 'package:flame_3d_workshop_slides/utils/code.dart';
import 'package:flutter/material.dart';
import 'package:flutter_deck/flutter_deck.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Code.init();
runApp(const FlutterDeckExample());
}
@@ -15,7 +18,15 @@ class FlutterDeckExample extends StatelessWidget {
// This is an entry point for the Flutter Deck app.
return FlutterDeckApp(
themeMode: ThemeMode.dark,
darkTheme: FlutterDeckThemeData.dark(),
darkTheme: FlutterDeckThemeData.fromTheme(
ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF16222A),
brightness: Brightness.dark,
),
useMaterial3: true,
),
),
configuration: const FlutterDeckConfiguration(
background: FlutterDeckBackgroundConfiguration(
dark: FlutterDeckBackground.solid(Color(0xFF16222A)),
@@ -24,6 +35,8 @@ class FlutterDeckExample extends StatelessWidget {
slides: const [
Slide00Intro(),
Slide01Bg(),
Slide02Code(),
Slide03Game(),
],
);
}
@@ -0,0 +1,24 @@
import 'package:flame_3d_workshop_slides/utils/code.dart';
import 'package:flutter/material.dart';
import 'package:flutter_deck/flutter_deck.dart';
class Slide02Code extends FlutterDeckSlideWidget {
const Slide02Code({super.key})
: super(
configuration: const FlutterDeckSlideConfiguration(
route: '/slide_02_code',
header: FlutterDeckHeaderConfiguration(title: 'Flame 3D Workshop'),
),
);
@override
Widget build(BuildContext context) {
return FlutterDeckSlide.blank(
builder: (context) => Center(
child: Text.rich(
Code.highlight('print("Hello, World!");'),
),
),
);
}
}
@@ -0,0 +1,25 @@
import 'package:flame/game.dart';
import 'package:flame_3d_workshop_slides/game/playground.dart';
import 'package:flutter/material.dart';
import 'package:flutter_deck/flutter_deck.dart';
class Slide03Game extends FlutterDeckSlideWidget {
const Slide03Game({super.key})
: super(
configuration: const FlutterDeckSlideConfiguration(
route: '/slide_03_game',
header: FlutterDeckHeaderConfiguration(title: 'Flame 3D Workshop'),
),
);
@override
Widget build(BuildContext context) {
return FlutterDeckSlide.blank(
builder: (context) => const Center(
child: GameWidget.controlled(
gameFactory: Playground.new,
),
),
);
}
}
+2
View File
@@ -1,2 +1,4 @@
export 'slide_00_intro.dart';
export 'slide_01_bg.dart';
export 'slide_02_code.dart';
export 'slide_03_game.dart';
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:syntax_highlight/syntax_highlight.dart';
class Code {
static late Highlighter _highlighter;
static Future<void> init() async {
await Highlighter.initialize(['dart']);
final theme = await HighlighterTheme.loadDarkTheme();
_highlighter = Highlighter(
language: 'dart',
theme: theme,
);
}
static TextSpan highlight(String code) {
return _highlighter.highlight(code);
}
}
@@ -6,13 +6,19 @@ import FlutterMacOS
import Foundation
import audioplayers_darwin
import device_info_plus
import irondash_engine_context
import path_provider_foundation
import screen_retriever_macos
import super_native_extensions
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}
+18
View File
@@ -2,39 +2,57 @@ PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- irondash_engine_context (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- super_native_extensions (0.0.1):
- FlutterMacOS
- window_manager (0.2.0):
- FlutterMacOS
DEPENDENCIES:
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
EXTERNAL SOURCES:
audioplayers_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
FlutterMacOS:
:path: Flutter/ephemeral
irondash_engine_context:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
super_native_extensions:
:path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
audioplayers_darwin: 4027b33a8f471d996c13f71cb77f0b1583b5d923
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
+2
View File
@@ -28,5 +28,7 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>FLTEnableImpeller</key>
<true/>
</dict>
</plist>
+1
View File
@@ -17,6 +17,7 @@ dependencies:
flutter:
sdk: flutter
flutter_deck: ^0.21.0
syntax_highlight: ^0.5.0
dev_dependencies:
flame_lint: ^1.4.2
+81 -1
View File
@@ -153,6 +153,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
device_info_plus:
dependency: transitive
description:
name: device_info_plus
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.dev"
source: hosted
version: "11.5.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
ffi:
dependency: transitive
description:
@@ -202,7 +218,7 @@ packages:
source: hosted
version: "2.11.9"
flame_lint:
dependency: transitive
dependency: "direct dev"
description:
name: flame_lint
sha256: "11c92624996dce2aeec2a41d44a61d7a560d551e4b5e447e2ad0b3812e8c2836"
@@ -304,6 +320,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
irondash_engine_context:
dependency: transitive
description:
name: irondash_engine_context
sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7"
url: "https://pub.dev"
source: hosted
version: "0.5.5"
irondash_message_channel:
dependency: transitive
description:
name: irondash_message_channel
sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060
url: "https://pub.dev"
source: hosted
version: "0.7.0"
json_annotation:
dependency: transitive
description:
@@ -424,6 +456,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pixel_snap:
dependency: transitive
description:
name: pixel_snap
sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
platform:
dependency: transitive
description:
@@ -565,6 +605,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
super_clipboard:
dependency: transitive
description:
name: super_clipboard
sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16
url: "https://pub.dev"
source: hosted
version: "0.9.1"
super_native_extensions:
dependency: transitive
description:
name: super_native_extensions
sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569
url: "https://pub.dev"
source: hosted
version: "0.9.1"
synchronized:
dependency: transitive
description:
@@ -573,6 +629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.0"
syntax_highlight:
dependency: transitive
description:
name: syntax_highlight
sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
term_glyph:
dependency: transitive
description:
@@ -613,6 +677,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev"
source: hosted
version: "5.14.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
window_manager:
dependency: transitive
description:
+4 -2
View File
@@ -7,6 +7,7 @@ environment:
sdk: ">=3.8.0 <4.0.0"
dev_dependencies:
flame_lint: ^1.4.2
melos: ^7.1.0
melos:
@@ -20,6 +21,7 @@ melos:
steps:
- analyze
- format
- markdown-check
description: Run all static analysis checks.
analyze:
@@ -31,9 +33,9 @@ melos:
description: Run `dart format` checks for all packages.
markdown-check:
run: markdownlint . --config .markdownlint.yaml
run: markdownlint . --ignore-path .markdownlintignore --config .markdownlint.yaml
description: Runs the markdown linting check.
markdown-fix:
run: markdownlint . --fix --config .markdownlint.yaml
run: markdownlint . --fix --ignore-path .markdownlintignore --config .markdownlint.yaml
description: Fixes the markdown linting errors.