Improves ASCII rasterization speed and simplifies API
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -38,9 +38,9 @@ void main() async {
|
||||
|
||||
final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true);
|
||||
final SixelRasterizer sixelRasterizer = SixelRasterizer();
|
||||
Rasterizer rasterizer = sixelRasterizer;
|
||||
CliRasterizer rasterizer = sixelRasterizer;
|
||||
|
||||
FrameBuffer buffer = FrameBuffer(
|
||||
final FrameBuffer initialFrameBuffer = FrameBuffer(
|
||||
stdout.terminalColumns,
|
||||
stdout.terminalLines,
|
||||
);
|
||||
@@ -49,6 +49,7 @@ void main() async {
|
||||
data: availableGames.values.first,
|
||||
difficulty: Difficulty.medium,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: initialFrameBuffer,
|
||||
audio: CliSilentAudio(),
|
||||
input: CliInput(),
|
||||
onGameWon: () {
|
||||
@@ -83,24 +84,17 @@ void main() async {
|
||||
if (stdout.hasTerminal) {
|
||||
int cols = stdout.terminalColumns;
|
||||
int rows = stdout.terminalLines;
|
||||
if (!rasterizer.isTerminalSizeSupported(cols, rows)) {
|
||||
if (!rasterizer.prepareTerminalFrame(engine, columns: cols, rows: rows)) {
|
||||
// Clear the screen and print the warning at the top left
|
||||
stdout.write('\x1b[2J\x1b[H');
|
||||
stdout.write('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n');
|
||||
stdout.write('${rasterizer.terminalSizeRequirement}\n');
|
||||
stdout.write(
|
||||
'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n',
|
||||
rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows),
|
||||
);
|
||||
stdout.write('Please resize your window to resume the game...');
|
||||
|
||||
// Prevent the engine from simulating a massive time jump when resumed
|
||||
lastTick = stopwatch.elapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.width != cols || buffer.height != rows) {
|
||||
buffer = FrameBuffer(cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Normal Game Loop
|
||||
@@ -112,8 +106,6 @@ void main() async {
|
||||
stdout.write('\x1b[H');
|
||||
|
||||
engine.tick(elapsed);
|
||||
rasterizer.render(engine, buffer);
|
||||
|
||||
stdout.write(rasterizer.finalizeFrame());
|
||||
stdout.write(rasterizer.render(engine));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class _GameScreenState extends State<GameScreen> {
|
||||
data: widget.data,
|
||||
difficulty: widget.difficulty,
|
||||
startingEpisode: widget.startingEpisode,
|
||||
frameBuffer: FrameBuffer(320, 200),
|
||||
audio: widget.audio,
|
||||
input: widget.input,
|
||||
onGameWon: () => Navigator.of(context).pop(),
|
||||
|
||||
@@ -59,7 +59,7 @@ class ColoredChar {
|
||||
int get argb => (0xFF000000) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
class AsciiRasterizer extends Rasterizer {
|
||||
class AsciiRasterizer extends CliRasterizer<dynamic> {
|
||||
static const double _targetAspectRatio = 4 / 3;
|
||||
static const int _terminalBackdropArgb = 0xFF009688;
|
||||
static const int _minimumTerminalColumns = 80;
|
||||
@@ -121,14 +121,16 @@ class AsciiRasterizer extends Rasterizer {
|
||||
|
||||
// Intercept the base render call to initialize our text grid
|
||||
@override
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
dynamic render(WolfEngine engine) {
|
||||
_engine = engine;
|
||||
_screen = List.generate(
|
||||
buffer.height,
|
||||
(_) =>
|
||||
List.filled(buffer.width, ColoredChar(' ', ColorPalette.vga32Bit[0])),
|
||||
engine.frameBuffer.height,
|
||||
(_) => List.filled(
|
||||
engine.frameBuffer.width,
|
||||
ColoredChar(' ', ColorPalette.vga32Bit[0]),
|
||||
),
|
||||
);
|
||||
return super.render(engine, buffer);
|
||||
return super.render(engine);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -630,7 +632,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
@override
|
||||
String finalizeFrame() {
|
||||
dynamic finalizeFrame() {
|
||||
if (_engine.player.damageFlash > 0.0) {
|
||||
if (isTerminal) {
|
||||
_applyDamageFlashToScene();
|
||||
@@ -640,8 +642,9 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
if (isTerminal) {
|
||||
_composeTerminalScene();
|
||||
return toAnsiString();
|
||||
}
|
||||
return toAnsiString();
|
||||
return _screen;
|
||||
}
|
||||
|
||||
// --- PRIVATE HUD DRAWING HELPERS ---
|
||||
@@ -787,7 +790,7 @@ class AsciiRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
/// Converts the current frame to a single printable ANSI string
|
||||
String toAnsiString() {
|
||||
StringBuffer toAnsiString() {
|
||||
StringBuffer buffer = StringBuffer();
|
||||
|
||||
int? lastForeground;
|
||||
@@ -828,6 +831,6 @@ class AsciiRasterizer extends Rasterizer {
|
||||
// Reset the terminal color at the very end
|
||||
buffer.write('\x1b[0m');
|
||||
|
||||
return buffer.toString();
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
/// Shared terminal orchestration for CLI rasterizers.
|
||||
abstract class CliRasterizer<T> extends Rasterizer<T> {
|
||||
/// Resolves the framebuffer dimensions required by this renderer.
|
||||
///
|
||||
/// The default uses the full terminal size.
|
||||
({int width, int height}) terminalFrameBufferSize(int columns, int rows) {
|
||||
return (width: columns, height: rows);
|
||||
}
|
||||
|
||||
/// Applies terminal-size policy and updates the engine framebuffer.
|
||||
///
|
||||
/// Returns `false` when the terminal is too small for this renderer.
|
||||
bool prepareTerminalFrame(
|
||||
WolfEngine engine, {
|
||||
required int columns,
|
||||
required int rows,
|
||||
}) {
|
||||
if (!isTerminalSizeSupported(columns, rows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final size = terminalFrameBufferSize(columns, rows);
|
||||
engine.setFrameBuffer(size.width, size.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Builds the standard terminal size warning shown by the CLI host.
|
||||
String buildTerminalSizeWarning({required int columns, required int rows}) {
|
||||
return '\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n'
|
||||
'$terminalSizeRequirement\n'
|
||||
'Current size: \x1b[33m${columns}x$rows\x1b[0m\n\n'
|
||||
'Please resize your window to resume the game...';
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
abstract class Rasterizer {
|
||||
abstract class Rasterizer<T> {
|
||||
late List<double> zBuffer;
|
||||
late int width;
|
||||
late int height;
|
||||
@@ -40,9 +40,9 @@ abstract class Rasterizer {
|
||||
|
||||
/// The main entry point called by the game loop.
|
||||
/// Orchestrates the mathematical rendering pipeline.
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
width = buffer.width;
|
||||
height = buffer.height;
|
||||
T render(WolfEngine engine) {
|
||||
width = engine.frameBuffer.width;
|
||||
height = engine.frameBuffer.height;
|
||||
// The 3D view typically takes up the top 80% of the screen
|
||||
viewHeight = (height * 0.8).toInt();
|
||||
zBuffer = List.filled(projectionWidth, 0.0);
|
||||
@@ -101,7 +101,7 @@ abstract class Rasterizer {
|
||||
void drawHud(WolfEngine engine);
|
||||
|
||||
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
||||
dynamic finalizeFrame();
|
||||
T finalizeFrame();
|
||||
|
||||
// ===========================================================================
|
||||
// SHARED LIGHTING MATH
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
class SixelRasterizer extends Rasterizer {
|
||||
class SixelRasterizer extends CliRasterizer<String> {
|
||||
static const double _targetAspectRatio = 4 / 3;
|
||||
static const int _defaultLineHeightPx = 18;
|
||||
static const double _defaultCellWidthToHeight = 0.55;
|
||||
@@ -85,12 +85,18 @@ class SixelRasterizer extends Rasterizer {
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
String render(WolfEngine engine) {
|
||||
_engine = engine;
|
||||
final FrameBuffer scaledBuffer = _createScaledBuffer(buffer);
|
||||
final FrameBuffer originalBuffer = engine.frameBuffer;
|
||||
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
|
||||
// We only need 8-bit indices for the 256 VGA colors
|
||||
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
|
||||
return super.render(engine, scaledBuffer);
|
||||
engine.frameBuffer = scaledBuffer;
|
||||
try {
|
||||
return super.render(engine);
|
||||
} finally {
|
||||
engine.frameBuffer = originalBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -3,16 +3,16 @@ import 'dart:math' as math;
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
class SoftwareRasterizer extends Rasterizer {
|
||||
class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
|
||||
late FrameBuffer _buffer;
|
||||
late WolfEngine _engine;
|
||||
|
||||
// Intercept the base render call to store our references
|
||||
@override
|
||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||
FrameBuffer render(WolfEngine engine) {
|
||||
_engine = engine;
|
||||
_buffer = buffer;
|
||||
return super.render(engine, buffer);
|
||||
_buffer = engine.frameBuffer;
|
||||
return super.render(engine);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,6 +18,7 @@ class WolfEngine {
|
||||
required this.onGameWon,
|
||||
required this.audio,
|
||||
required this.input,
|
||||
required this.frameBuffer,
|
||||
}) : doorManager = DoorManager(
|
||||
onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
|
||||
);
|
||||
@@ -48,6 +49,9 @@ class WolfEngine {
|
||||
/// Polls and processes raw user input into actionable engine commands.
|
||||
final Wolf3dInput input;
|
||||
|
||||
/// The shared render target used by all renderer frontends.
|
||||
FrameBuffer frameBuffer;
|
||||
|
||||
/// Handles the detection and movement of secret "Pushwalls".
|
||||
final PushwallManager pushwallManager = PushwallManager();
|
||||
|
||||
@@ -86,6 +90,17 @@ class WolfEngine {
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/// Replaces the shared framebuffer when dimensions change.
|
||||
void setFrameBuffer(int width, int height) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw ArgumentError('FrameBuffer dimensions must be greater than zero.');
|
||||
}
|
||||
if (frameBuffer.width == width && frameBuffer.height == height) {
|
||||
return;
|
||||
}
|
||||
frameBuffer = FrameBuffer(width, height);
|
||||
}
|
||||
|
||||
/// The primary heartbeat of the engine.
|
||||
///
|
||||
/// Updates all world subsystems based on the [elapsed] time.
|
||||
|
||||
@@ -11,6 +11,7 @@ export 'src/engine/managers/pushwall_manager.dart';
|
||||
export 'src/engine/player/player.dart';
|
||||
export 'src/engine/rasterizer/ascii_rasterizer.dart'
|
||||
show AsciiRasterizer, ColoredChar;
|
||||
export 'src/engine/rasterizer/cli_rasterizer.dart';
|
||||
export 'src/engine/rasterizer/rasterizer.dart';
|
||||
export 'src/engine/rasterizer/sixel_rasterizer.dart';
|
||||
export 'src/engine/rasterizer/software_rasterizer.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_renderer/base_renderer.dart';
|
||||
|
||||
@@ -14,19 +13,28 @@ class WolfAsciiRenderer extends BaseWolfRenderer {
|
||||
}
|
||||
|
||||
class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
|
||||
static const int _renderWidth = 160;
|
||||
static const int _renderHeight = 100;
|
||||
|
||||
List<List<ColoredChar>> _asciiFrame = [];
|
||||
final AsciiRasterizer _asciiRasterizer = AsciiRasterizer();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.engine.frameBuffer.width != _renderWidth ||
|
||||
widget.engine.frameBuffer.height != _renderHeight) {
|
||||
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64);
|
||||
|
||||
@override
|
||||
void performRender() {
|
||||
setState(() {
|
||||
_asciiFrame = _asciiRasterizer.render(
|
||||
widget.engine,
|
||||
FrameBuffer(160, 100),
|
||||
);
|
||||
_asciiFrame = _asciiRasterizer.render(widget.engine);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,22 @@ class WolfFlutterRenderer extends BaseWolfRenderer {
|
||||
|
||||
class _WolfFlutterRendererState
|
||||
extends BaseWolfRendererState<WolfFlutterRenderer> {
|
||||
final FrameBuffer _frameBuffer = FrameBuffer(320, 200);
|
||||
static const int _renderWidth = 320;
|
||||
static const int _renderHeight = 200;
|
||||
final SoftwareRasterizer _rasterizer = SoftwareRasterizer();
|
||||
|
||||
ui.Image? _renderedFrame;
|
||||
bool _isRendering = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.engine.frameBuffer.width != _renderWidth ||
|
||||
widget.engine.frameBuffer.height != _renderHeight) {
|
||||
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64);
|
||||
|
||||
@@ -32,12 +42,13 @@ class _WolfFlutterRendererState
|
||||
if (_isRendering) return;
|
||||
_isRendering = true;
|
||||
|
||||
_rasterizer.render(widget.engine, _frameBuffer);
|
||||
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
|
||||
_rasterizer.render(widget.engine);
|
||||
|
||||
ui.decodeImageFromPixels(
|
||||
_frameBuffer.pixels.buffer.asUint8List(),
|
||||
_frameBuffer.width,
|
||||
_frameBuffer.height,
|
||||
frameBuffer.pixels.buffer.asUint8List(),
|
||||
frameBuffer.width,
|
||||
frameBuffer.height,
|
||||
ui.PixelFormat.rgba8888,
|
||||
(ui.Image image) {
|
||||
if (mounted) {
|
||||
|
||||
Reference in New Issue
Block a user