1 /// 2 module isodi.raylib.camera; 3 4 import std.conv; 5 import std.meta; 6 import std.traits; 7 8 import raylib; 9 10 import isodi.camera : Camera; 11 import isodi.object3d; 12 13 14 @safe: 15 16 17 private { 18 19 struct Affects(string what) { 20 static immutable name = what; 21 } 22 struct Change(short by) { 23 static immutable value = by; 24 } 25 enum Speed; 26 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!"angle.x" { 46 47 /// Rotate the camera clockwise. 48 @Change!(-1) 49 KeyboardKey rotateRight; 50 51 /// Rotate the camera counter-clockwise. 52 @Change!(+1) 53 KeyboardKey rotateLeft; 54 55 } 56 57 @Affects!"angle.y" { 58 59 /// Rotate the camera down on the Y axis. 60 @Change!(-1) 61 KeyboardKey rotateDown; 62 63 /// Rotate the camera up on the Y axis. 64 @Change!(+1) 65 KeyboardKey rotateUp; 66 67 } 68 69 @Affects!"offsetScreenX" { 70 71 /// Move the camera towards negative X. 72 @Change!(-1) 73 KeyboardKey moveLeft; 74 75 /// Move the camera towards positive X. 76 @Change!(+1) 77 KeyboardKey moveRight; 78 79 } 80 81 @Affects!"offsetScreenY" { 82 83 /// Move the camera towards positive Y. 84 @Change!(+1) 85 KeyboardKey moveDown; 86 87 /// Move the camera towards negative Y. 88 @Change!(-1) 89 KeyboardKey moveUp; 90 91 } 92 93 @Affects!"offset.height" { 94 95 /// Move the camera downwards. 96 @Change!(-1) 97 KeyboardKey moveBelow; 98 99 /// Move the camera upwards. 100 @Change!(+1) 101 KeyboardKey moveAbove; 102 103 } 104 105 @Speed { 106 107 /// Zoom speed, cell per second. 108 @Affects!"distance" 109 float zoomSpeed = 10; 110 111 /// Rotation speed, degrees per second. 112 @Affects!"angle.x" 113 @Affects!"angle.y" 114 float rotateSpeed = 90; 115 116 /// Movement speed, cells per second. 117 @Affects!"offsetScreenX" 118 @Affects!"offsetScreenY" 119 @Affects!"offset.height" 120 float movementSpeed = 4; 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 /// Helper for quickly binding keys to freely move the camera. 133 void updateCamera(ref Camera camera, CameraKeybindings keybinds) @trusted { 134 135 assert(camera.follow !is null, 136 "camera.follow must be set for updateCamera to work, and it must use the Object3D.Implement mixin"); 137 138 alias SpeedFields = getSymbolsByUDA!(CameraKeybindings, Speed); 139 140 const delta = GetFrameTime(); 141 142 // Check each field 143 static foreach (actionName; FieldNameTuple!CameraKeybindings) {{ 144 145 alias BindField = mixin("CameraKeybindings." ~ actionName); 146 147 // This is a key field 148 static if (hasUDA!(BindField, Change)) { 149 150 alias ChangeDeco = getUDAs!(BindField, Change)[0]; 151 enum direction = ChangeDeco.value; 152 153 // Pressed 154 if (mixin("keybinds." ~ actionName).IsKeyDown) { 155 156 // Get the affected field 157 alias AffectDeco = getUDAs!(BindField, Affects)[0]; 158 enum cameraField = "camera." ~ AffectDeco.name; 159 alias CameraField = typeof(mixin(cameraField)); 160 161 // Get the speed field for the deco 162 alias HasAffectDeco = ApplyRight!(hasUDA, AffectDeco); 163 alias speedSymbol = Filter!(HasAffectDeco, SpeedFields)[0]; 164 enum speedField = __traits(identifier, speedSymbol); 165 166 // Get the total change 167 const change = direction * delta * mixin("keybinds." ~ speedField); 168 169 // Check if the given type is a field or property 170 enum IsField(alias T) = !isFunction!T || functionAttributes!T & FunctionAttribute.property; 171 172 // If so 173 static if (IsField!(mixin(cameraField))) { 174 175 import std.algorithm : clamp; 176 177 // Get the new value 178 auto newValue = mixin(cameraField) + change; 179 180 // Clamp angle Y 181 static if (cameraField == "camera.angle.y") { 182 183 newValue = newValue.clamp(0, 90); 184 185 } 186 187 // Clamp camera distance 188 else static if (cameraField == "camera.distance") { 189 190 newValue = newValue.clamp(keybinds.zoomInLimit, keybinds.zoomOutLimit); 191 192 } 193 194 // Increment the field 195 mixin(cameraField) = newValue; 196 197 } 198 199 // Call the function otherwise 200 else mixin(cameraField)(change); 201 202 } 203 204 } 205 206 }} 207 208 } 209 210 /// Apply a billboard effect on the current object. 211 void applyBillboard(ref Camera camera) @trusted { 212 213 // Rotate to counter the camera 214 rlRotatef(camera.angle.x, 0, 1, 0); 215 rlRotatef(-camera.angle.y, 1, 0, 0); 216 217 }