Actors, brains and health: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: brain (line 14), live (line 110), hit (line 127), actor (line 183), hurt (line 192).
1/// Who a collider is, and the components that make an actor: health that can
2/// run out, and a brain that has memory of its own.
3///
4/// Quoted by `actors.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:flutter3d_sim/flutter3d_sim.dart';
12import 'package:vector_math/vector_math.dart';
14/// A brain with one bit of memory: whether it has ever been hurt.
15final class _WaryBrain extends Brain {
16 bool everHurt = false;
19 void onHurt(Mind it, double amount) => everHurt = true;
22 Map<String, Object?> save() => <String, Object?>{'everHurt': everHurt};
25 void restore(Map<String, Object?> from) =>
26 everHurt = from['everHurt'] == true;
27}
29final class ActorsDemo extends ShowcaseDemo {
30 late final double _currentHealth;
31 late final bool _stillAlive;
32 late final bool _rememberedBeingHurt;
34 late final Actor _goblin;
35 late final _WaryBrain _brain;
36 late final MeshNode _body;
37 late final MeshNode _bar;
38 late final Material _skin;
39 double _hitAsked = 0.0;
40 bool _resetAsked = false;
41 double _flash = 0.0;
43 static const double _barWidth = 1.4;
46 void configureView(DemoContext context) {
47 context.orbit
48 ..distance = 4.2
49 ..pitch = 0.25
50 ..yaw = 0.4;
51 context.orbit.target.setValues(0.0, 0.6, 0.0);
52 }
55 Scene build(DemoContext context) {
56 final (
57 String _,
58 double currentHealth,
59 bool stillAlive,
60 bool rememberedBeingHurt,
61 ) = _run();
62 _currentHealth = currentHealth;
63 _stillAlive = stillAlive;
64 _rememberedBeingHurt = rememberedBeingHurt;
66 _spawn();
67 _skin = Material(name: 'goblin', baseColor: _healthy);
68 _body = MeshNode(
69 DeviceMesh.upload(context.device, SphereShape(segments: 24).build()),
70 _skin,
71 name: 'goblin',
72 );
73 MeshNode block(String name, Vector3 size, Vector4 color) => MeshNode(
74 DeviceMesh.upload(context.device, CuboidShape(size: size).build()),
75 Material(name: name, baseColor: color),
76 name: name,
77 );
78 _bar = block(
79 'health',
80 Vector3(_barWidth, 0.14, 0.14),
81 Vector4(0.3, 0.8, 0.3, 1.0),
82 );
83 return Scene()
84 ..ambientColor = Vector3(0.5, 0.55, 0.65)
85 ..ambientIntensity = 0.3
86 ..add(
87 block('floor', Vector3(6.0, 0.1, 6.0), Vector4(0.36, 0.4, 0.38, 1.0))
88 ..setPosition(0.0, -0.05, 0.0),
89 )
90 ..add(
91 block(
92 'bar back',
93 Vector3(_barWidth + 0.08, 0.2, 0.1),
94 Vector4(0.1, 0.1, 0.12, 1.0),
95 )..setPosition(0.0, 1.5, -0.02),
96 )
97 ..add(_body)
98 ..add(_bar)
99 ..add(
100 LightNode(name: 'sun', intensity: 3.0)
101 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
102 );
103 }
105 static Vector4 get _healthy => Vector4(0.5, 0.7, 0.3, 1.0);
106 static Vector4 get _wary => Vector4(0.85, 0.6, 0.25, 1.0);
108 /// The goblin of the page, built the way the first step builds one.
109 void _spawn() {
110 final EcsWorld entities = EcsWorld();
111 final entity = entities.spawn();
112 _brain = _WaryBrain();
113 entities
114 ..set(entity, Vitality(Health(30.0)))
115 ..set(entity, Thinking(_brain));
116 _goblin = Actor(entities, entity, name: 'goblin');
117 }
120 void update(DemoContext context, double dt) {
121 if (_resetAsked) {
122 _resetAsked = false;
123 _spawn();
124 _flash = 0.0;
125 }
126 if (_hitAsked > 0.0) {
127 // The damage goes through `Health`; the brain hears of it separately.
128 _goblin.applyDamage(_hitAsked);
129 _brain.onHurt(Mind(_stubSystem()), _hitAsked);
130 _hitAsked = 0.0;
131 _flash = 1.0;
132 }
133 _flash = math.max(0.0, _flash - dt * 3.0);
135 final double fraction = (_goblin.health!.current / _goblin.health!.maximum)
136 .clamp(0.0, 1.0);
137 // The bar shrinks towards its left end, and goes red as it does.
138 _bar
139 ..setScale(math.max(fraction, 0.001), 1.0, 1.0)
140 ..setPosition(-(1.0 - fraction) * _barWidth / 2, 1.5, 0.05);
141 _bar.material.baseColor.setValues(
142 1.0 - fraction * 0.7,
143 0.2 + 0.6 * fraction,
144 0.2,
145 1.0,
146 );
148 if (_goblin.isAlive) {
149 // A hit squashes it, and a goblin that has been hurt stays wary.
150 final double squash = 1.0 - 0.25 * _flash;
151 _body
152 ..setScale(1.0 + 0.2 * _flash, squash, 1.0 + 0.2 * _flash)
153 ..setPosition(0.0, 0.5 * squash, 0.0);
154 _skin.baseColor.setFrom(_brain.everHurt ? _wary : _healthy);
155 } else {
156 // Dead: flat on the floor and grey.
157 _body
158 ..setScale(1.2, 0.2, 1.2)
159 ..setPosition(0.0, 0.1, 0.0);
160 _skin.baseColor.setValues(0.35, 0.35, 0.37, 1.0);
161 }
162 }
165 List<DemoControl> controls(DemoContext context) => <DemoControl>[
166 ToggleControl(
167 'Hit it for 12',
168 value: () => false,
169 onChanged: (bool v) {
170 if (v) _hitAsked = 12.0;
171 },
172 ),
173 ToggleControl(
174 'A fresh goblin',
175 value: () => false,
176 onChanged: (bool v) {
177 if (v) _resetAsked = true;
178 },
179 ),
180 ];
182 static (String, double, bool, bool) _run() {
183 final entities = EcsWorld();
184 final entity = entities.spawn();
185 entities
186 ..set(entity, Vitality(Health(30.0)))
187 ..set(entity, Thinking(_WaryBrain()));
188 final goblin = Actor(entities, entity, name: 'goblin');
190 final aliveBefore = goblin.isAlive;
192 final brain = goblin.brain! as _WaryBrain;
193 goblin.applyDamage(12.0);
194 brain.onHurt(Mind(_stubSystem()), 12.0);
195 final currentHealth = goblin.health!.current;
197 final report =
198 'a fresh goblin is alive: $aliveBefore, has ${goblin.health!.current} '
199 'health of ${goblin.health!.maximum}\n'
200 'after twelve damage: $currentHealth health, still alive: '
201 '${goblin.isAlive}, its brain remembers being hurt: ${brain.everHurt}';
202 return (report, currentHealth, goblin.isAlive, brain.everHurt);
203 }
205 /// A real `ActorSystem` needs a live level; `Brain.onHurt` only needs a
206 /// `Mind`, and a `Mind` only needs a system to ask `focus` of, which this
207 /// page never calls. A throwaway system is enough to build one.
208 static ActorSystem _stubSystem() =>
209 ActorSystem(world: CollisionWorld(), random: GameRandom(1));
212 void verify(Scene scene, FrameResult frame) {
213 if (frame.drawCalls < 1) {
214 throw StateError('the goblin marker was not drawn');
215 }
216 // Compared as numbers, not read back out of `_report`: a whole-number
217 // double loses its trailing `.0` when a web backend formats it, and a
218 // compiled `18` failing a substring match against `'18.0 health'` would
219 // be this check catching its own string, not the damage arithmetic.
220 if (_currentHealth != 18.0 || !_stillAlive) {
221 throw StateError(
222 'twelve damage on thirty health should leave the '
223 'goblin alive at eighteen',
224 );
225 }
226 if (!_rememberedBeingHurt) {
227 throw StateError('the brain should remember being hurt');
228 }
229 }
230}