Line data Source code
1 : import 'dart:math' as math; 2 : import 'dart:ui'; 3 : 4 : import 'package:flutter/painting.dart'; 5 : 6 : import '../../extensions.dart'; 7 : import '../../game.dart'; 8 : import 'projector.dart'; 9 : 10 : /// A viewport is a class that potentially translates and resizes the screen. 11 : /// The reason you might want to have a viewport is to make sure you handle any 12 : /// screen size and resolution correctly depending on your needs. 13 : /// 14 : /// Not only screens can have endless configurations of width and height with 15 : /// different ratios, you can also embed games as widgets within a Flutter app. 16 : /// In fact, the size of the game can even change dynamically (if the layout 17 : /// changes or in desktop, for example). 18 : /// 19 : /// For some simple games, that is not an issue. The game will just adapt 20 : /// to fit the screen, so if the game world is 1:1 with screen it will just 21 : /// be bigger or smaller. But if you want a consistent experience across 22 : /// platforms and players, you should use a viewport. 23 : /// 24 : /// When using a viewport, [resize] should be called by the engine with 25 : /// the raw canvas size (on startup and subsequent resizes) and that will 26 : /// configure [getEffectiveSize()] and [getCanvasSize()]. 27 : /// The Viewport can also apply an offset to render and clip the canvas adding 28 : /// borders (clipping) when necessary. 29 : /// When rendering, call [render] and put all your rendering inside the lambda 30 : /// so that the correct transformations are applied. 31 : /// 32 : /// You can think of a Viewport as mechanism to watch a wide-screen movie on a 33 : /// square monitor. You can stretch the movie to fill the square, but the width 34 : /// and height will be stretched by different amounts, causing distortion. You 35 : /// can fill in the smallest dimension and crop the biggest (that causes 36 : /// cropping). Or you can fill in the biggest and add black bars to cover the 37 : /// unused space on the smallest (this is the [FixedResolutionViewport]). 38 : /// 39 : /// The other option is to not use a viewport ([DefaultViewport]) and have 40 : /// your game dynamically render itself to fill in the existing space (basically 41 : /// this means generating either a wide-screen or a square movie on the fly). 42 : /// The disadvantage is that different players on different devices will see 43 : /// different games. For example a hidden door because it's too far away to 44 : /// render in Screen 1 might be visible on Screen 2. Specially if it's an 45 : /// online/competitive game, it can give unfair advantages to users with certain 46 : /// screen resolutions. If you want to "play director" and know exactly what 47 : /// every player is seeing at every time, you should use a Viewport. 48 : abstract class Viewport extends Projector { 49 : /// This configures the viewport with a new raw canvas size. 50 : /// It should immediately affect [effectiveSize] and [canvasSize]. 51 : /// This must be called by the engine at startup and also whenever the 52 : /// size changes. 53 : void resize(Vector2 newCanvasSize); 54 : 55 : /// This transforms the canvas so that the coordinate system is viewport- 56 : /// -aware. All your rendering logic should be put inside the lambda. 57 : void render(Canvas c, void Function(Canvas c) renderGame); 58 : 59 : /// This returns the effective size, after viewport transformation. 60 : /// This is not the game widget size but for all intents and purposes, 61 : /// inside your game, this size should be used as the real one. 62 : Vector2 get effectiveSize; 63 : 64 : /// This returns the real widget size (well actually the logical Flutter 65 : /// size of your widget). This is the raw canvas size as it would be without 66 : /// any viewport. 67 : /// 68 : /// You probably don't need to care about this if you are using a viewport. 69 : Vector2 get canvasSize; 70 : } 71 : 72 : /// This is the default viewport if you want no transformation. 73 : /// The raw canvasSize is just propagated to the effective size and no 74 : /// translation is applied. 75 : /// This basically no-ops the viewport. 76 : class DefaultViewport extends Viewport { 77 : @override 78 : late Vector2 canvasSize; 79 : 80 3 : @override 81 : void render(Canvas c, void Function(Canvas c) renderGame) { 82 3 : renderGame(c); 83 : } 84 : 85 24 : @override 86 : void resize(Vector2 newCanvasSize) { 87 24 : canvasSize = newCanvasSize; 88 : } 89 : 90 24 : @override 91 24 : Vector2 get effectiveSize => canvasSize; 92 : 93 1 : @override 94 : Vector2 projectVector(Vector2 vector) => vector; 95 : 96 5 : @override 97 : Vector2 unprojectVector(Vector2 vector) => vector; 98 : 99 1 : @override 100 : Vector2 scaleVector(Vector2 vector) => vector; 101 : 102 1 : @override 103 : Vector2 unscaleVector(Vector2 vector) => vector; 104 : } 105 : 106 : /// This is the most common viewport if you want to have full control of what 107 : /// the game looks like. Basically this viewport makes sure the ratio between 108 : /// width and height is *always* the same in your game, no matter the platform. 109 : /// 110 : /// To accomplish this you choose a virtual size that will always match the 111 : /// effective size. 112 : /// 113 : /// Under the hood, the Viewport will try to expand (or contract) the virtual 114 : /// size so that it fits the most of the screen as it can. So for example, 115 : /// if the viewport happens to be the same ratio of the screen, it will resize 116 : /// to fit 100%. But if they are different ratios, it will resize the most it 117 : /// can and then will add black (color is configurable) borders. 118 : /// 119 : /// Then, inside your game, you can always assume the game size is the fixed 120 : /// dimension that you provided. 121 : /// 122 : /// Normally you can pick a virtual size that has the same ratio as the most 123 : /// used device for your game (like a pretty standard mobile ratio if you 124 : /// are doing a mobile game) and then in most cases this will apply no 125 : /// transformation whatsoever, and if the a device with a different ratio is 126 : /// used it will try to adapt the best as possible. 127 : class FixedResolutionViewport extends Viewport { 128 : @override 129 : late Vector2 canvasSize; 130 : 131 : @override 132 : late Vector2 effectiveSize; 133 : 134 : final Vector2 _scaledSize = Vector2.zero(); 135 3 : Vector2 get scaledSize => _scaledSize.clone(); 136 : 137 : final Vector2 _resizeOffset = Vector2.zero(); 138 6 : Vector2 get resizeOffset => _resizeOffset.clone(); 139 : 140 : late double _scale; 141 4 : double get scale => _scale; 142 : 143 : /// The matrix used for scaling and translating the canvas 144 : final Matrix4 _transform = Matrix4.identity(); 145 : 146 : /// The Rect that is used to clip the canvas 147 : late Rect _clipRect; 148 : 149 2 : FixedResolutionViewport(this.effectiveSize); 150 : 151 2 : @override 152 : void resize(Vector2 newCanvasSize) { 153 2 : canvasSize = newCanvasSize; 154 : 155 4 : _scale = math.min( 156 10 : canvasSize.x / effectiveSize.x, 157 10 : canvasSize.y / effectiveSize.y, 158 : ); 159 : 160 2 : _scaledSize 161 4 : ..setFrom(effectiveSize) 162 4 : ..scale(_scale); 163 2 : _resizeOffset 164 4 : ..setFrom(canvasSize) 165 4 : ..sub(_scaledSize) 166 2 : ..scale(0.5); 167 : 168 8 : _clipRect = _resizeOffset & _scaledSize; 169 : 170 4 : _transform.setIdentity(); 171 12 : _transform.translate(_resizeOffset.x, _resizeOffset.y); 172 6 : _transform.scale(_scale); 173 : } 174 : 175 1 : @override 176 : void render(Canvas c, void Function(Canvas) renderGame) { 177 1 : c.save(); 178 2 : c.clipRect(_clipRect); 179 3 : c.transform(_transform.storage); 180 1 : renderGame(c); 181 1 : c.restore(); 182 : } 183 : 184 1 : @override 185 : Vector2 projectVector(Vector2 viewportCoordinates) { 186 4 : return (viewportCoordinates * _scale)..add(_resizeOffset); 187 : } 188 : 189 1 : @override 190 : Vector2 unprojectVector(Vector2 screenCoordinates) { 191 5 : return (screenCoordinates - _resizeOffset)..scale(1 / _scale); 192 : } 193 : 194 1 : @override 195 : Vector2 scaleVector(Vector2 viewportCoordinates) { 196 2 : return viewportCoordinates * scale; 197 : } 198 : 199 1 : @override 200 : Vector2 unscaleVector(Vector2 screenCoordinates) { 201 2 : return screenCoordinates / scale; 202 : } 203 : }