1 /// Optional helper utility for managing the camera. 2 module isodi.camera; 3 4 import raylib; 5 6 import std.conv; 7 import std.math; 8 import std.meta; 9 import std.traits; 10 11 private alias PI = std.math.PI; 12 13 14 @safe: 15 16 17 // Helper UDAs for metaprogramming 18 private { 19 20 struct Affects(string what) { 21 static immutable name = what; 22 } 23 struct Change(short by) { 24 static immutable value = by; 25 } 26 enum Speed; 27 28 } 29 30 /// Keys to connect to camera actions. 31 struct CameraKeybindings { 32 33 @Affects!"distance" { 34 35 /// Zoom the camera in. 36 @Change!(-1) 37 KeyboardKey zoomIn; 38 39 /// Zoom the camera out 40 @Change!(+1) 41 KeyboardKey zoomOut; 42 43 } 44 45 @Affects!"yaw" { 46 47 /// Rotate the camera around the Y axis clockwise. 48 @Change!(-1) 49 KeyboardKey rotateRight; 50 51 /// Rotate the camera around the Y axis counter-clockwise. 52 @Change!(+1) 53 KeyboardKey rotateLeft; 54 55 } 56 57 @Affects!"pitch" { 58 59 /// Pitch the camera down. 60 @Change!(-1) 61 KeyboardKey rotateDown; 62 63 /// Pitch the camera up. 64 @Change!(+1) 65 KeyboardKey rotateUp; 66 67 } 68 69 @Affects!"offsetScreenX" { 70 71 /// Move the camera to the left. 72 @Change!(-1) 73 KeyboardKey moveLeft; 74 75 /// Move the camera to the right. 76 @Change!(+1) 77 KeyboardKey moveRight; 78 79 } 80 81 @Affects!"offsetScreenZ" { 82 83 /// Move the camera backward. 84 @Change!(+1) 85 KeyboardKey moveBackward; 86 87 /// Move the camera forward. 88 @Change!(-1) 89 KeyboardKey moveForward; 90 91 } 92 93 @Affects!"offset.y" { 94 95 /// Move the camera downwards. 96 @Change!(-1) 97 KeyboardKey moveDown; 98 99 /// Move the camera upwards. 100 @Change!(+1) 101 KeyboardKey moveUp; 102 103 } 104 105 @Speed { 106 107 /// Zoom speed, cell per second. 108 @Affects!"distance" 109 float zoomSpeed = 15; 110 111 /// Rotation speed, radians per second. 112 @Affects!"yaw" 113 @Affects!"pitch" 114 float rotateSpeed = PI_2; 115 116 /// Movement speed, cells per second. 117 @Affects!"offsetScreenX" 118 @Affects!"offsetScreenZ" 119 @Affects!"offset.y" 120 float movementSpeed = 8; 121 122 } 123 124 /// Maximum zoom in. 125 float zoomInLimit = 5; 126 127 /// Maximum zoom out. This should be greater than `zoomInLimit`. 128 float zoomOutLimit = 50; 129 130 } 131 132 struct CameraController { 133 134 /// Camera yaw. 135 float yaw = PI_4; 136 137 /// Camera pitch. 138 /// 139 /// Must be between 0° (side view) and 90° (top-down), defaults to 45° 140 float pitch = PI_4; 141 142 /// Convience function to determine the camera's position. 143 /// 144 /// This can be null. If so, follows position (0, 0, 0). 145 Vector3 delegate() @safe follow; 146 147 /// Distance between the camera and the followed object. 148 float distance = 15; 149 150 /// Distance to FOV ratio, used to correct projection parameters. 151 float distanceFOVRatio = 5; 152 153 /// Offset between camera and the followed object. 154 Vector3 offset; 155 156 /// Change the offset relative to the screen 157 void offsetScreenX(float value) { 158 159 offset.x += value * yaw.cos; 160 offset.z += value * yaw.sin; 161 162 } 163 164 /// Change the offset relative to the screen 165 void offsetScreenZ(float value) { 166 167 offset.x += value * yaw.sin; 168 offset.z += value * yaw.cos; 169 170 } 171 172 /// Get input and update the camera. 173 void update(CameraKeybindings keybinds, ref Camera camera) { 174 175 input(keybinds); 176 output(camera); 177 178 } 179 180 /// ditto 181 Camera update(CameraKeybindings keybinds) { 182 183 Camera camera = { 184 up: Vector3(0, 1, 0), 185 projection: CameraProjection.CAMERA_ORTHOGRAPHIC, 186 }; 187 188 update(keybinds, camera); 189 190 return camera; 191 192 } 193 194 /// Move the camera using bound keys. 195 void input(CameraKeybindings keybinds) @trusted { 196 197 alias SpeedFields = getSymbolsByUDA!(CameraKeybindings, Speed); 198 199 const delta = GetFrameTime(); 200 201 // Check each field 202 static foreach (actionName; FieldNameTuple!CameraKeybindings) {{ 203 204 alias BindField = mixin("CameraKeybindings." ~ actionName); 205 206 // This is a key field 207 static if (hasUDA!(BindField, Change)) { 208 209 alias ChangeDeco = getUDAs!(BindField, Change)[0]; 210 enum direction = ChangeDeco.value; 211 212 // Pressed 213 if (mixin("keybinds." ~ actionName).IsKeyDown) { 214 215 // Get the affected field 216 alias AffectDeco = getUDAs!(BindField, Affects)[0]; 217 enum controllerField = "this." ~ AffectDeco.name; 218 alias ControllerField = typeof(mixin(controllerField)); 219 220 // Get the speed field for the deco 221 alias HasAffectDeco = ApplyRight!(hasUDA, AffectDeco); 222 alias speedSymbol = Filter!(HasAffectDeco, SpeedFields)[0]; 223 enum speedField = __traits(identifier, speedSymbol); 224 225 // Get the total change 226 const change = direction * delta * mixin("keybinds." ~ speedField); 227 228 // Check if the given type is a field or property 229 enum IsField(alias T) = !isFunction!T || functionAttributes!T & FunctionAttribute.property; 230 231 // If so 232 static if (IsField!(mixin(controllerField))) { 233 234 import std.algorithm : clamp; 235 236 // Get the new value 237 auto newValue = mixin(controllerField) + change; 238 239 // Clamp camera distance 240 static if (AffectDeco.name == "distance") { 241 242 newValue = newValue.clamp(keybinds.zoomInLimit, keybinds.zoomOutLimit); 243 244 } 245 246 // Clamp pitch 247 else static if (AffectDeco.name == "pitch") { 248 249 newValue = newValue.clamp(0, PI_2 - 2e-6); 250 251 } 252 253 // Update the field 254 mixin(controllerField) = newValue; 255 256 } 257 258 // Call the function otherwise 259 else mixin(controllerField)(change); 260 261 } 262 263 } 264 265 }} 266 267 } 268 269 /// Update the camera based on controller data. 270 void output(ref Camera camera) const { 271 272 // Calculate the target 273 const target = follow is null 274 ? Vector3() 275 : follow(); 276 277 // Update the camera 278 // not sure how to get the correct fovy from distance, this is just close to the expected result. 279 // TODO: figure it out. 280 camera.fovy = distance; 281 282 // Get the target 283 camera.target = target + offset; 284 285 // And place the camera 286 camera.position = camera.target + Vector3( 287 pitch.cos * yaw.sin, 288 pitch.sin, 289 pitch.cos * yaw.cos, 290 ) * (distance * distanceFOVRatio); 291 292 } 293 294 }