In the last article we built a camera that can move around our grid, rotate, and zoom in and out. The camera works pretty well, but we need to make sure its movement isn’t tied to the frame rate. While we’re improving this code, we should also place bounds on the camera to keep it attached to the grid.
Smoothing Zoom
Let’s start by smoothing the zoom function. In order to do this, we need to change our zoom_level parameter to be a f32 instead of a usize. We will make the value be in the range [0.0, 1.0] and in order to calculate the offset, we will mix the two closest zoom_level_offset positions to determine the offset. We will scale the delta to the zoom level in each frame by the time delta. By doing this, our zoom function will no longer be tied to frame rate.
#[derive(Debug, Component)]pubstructCameraTarget{/// value in range [0.0, 1.0] which determines which zoom_level_offsets to use
zoom_level: f32,/// list of offsets to position the camera relative to the look_at point
zoom_level_offsets: Vec<Vec3>,/// point in space the camera should look at
look_at: Vec3,/// rotation angle around the up axis
rotation: f32,/// normal vector representing up for the camera
up: Vec3,/// true wheh zoom_level, look_at, or up change
/// this let's our system know to update the camera transform in the scene
is_dirty: bool,}...implCameraTarget{pubfnupdate(mutscroll_evr: EventReader<MouseWheel>,keys: Res<Input<KeyCode>>,time: Res<Time>,mutcamera_query: Query<(&mutCameraTarget,&mutTransform),With<Camera>>,){letdelta_y: f32=scroll_evr.read().map(|ev|ev.y).sum();letmutdelta_zoom_level: f32=ifdelta_y<0.0{-1.0}elseifdelta_y>0.0{1.0}else{0.0};ifkeys.pressed(KeyCode::Equals)||keys.pressed(KeyCode::Plus){delta_zoom_level=-1.0;}ifkeys.pressed(KeyCode::Minus){delta_zoom_level=1.0;}...ifdelta_zoom_level!=0.0{target.change_zoom_to(delta_zoom_level*time.delta_seconds().clamp(0.0,1.0));}...}pubfnupdate_transform(&mutself,transform: &mutTransform){ifself.is_dirty{letzoom_level_a=self.zoom_level_offsets[(self.zoom_level*(self.zoom_level_offsets.len()-1)asf32).floor()asusize];letzoom_level_b=self.zoom_level_offsets[(self.zoom_level*(self.zoom_level_offsets.len()-1)asf32).ceil()asusize];letmutmix=self.zoom_level*(self.zoom_level_offsets.len()-1)asf32;mix-=mixasu32asf32;letoffset=zoom_level_a.lerp(zoom_level_b,mix);transform.translation=self.look_at+offset;transform.rotate_around(self.look_at,Quat::from_axis_angle(self.up,self.rotation));transform.look_at(self.look_at,self.up);self.is_dirty=false;}}...pubfnchange_zoom_to(&mutself,delta_zoom_level: f32){self.zoom_to(self.zoom_level+delta_zoom_level)}pubfnzoom_to(&mutself,zoom_level: f32){letold_zoom_level=self.zoom_level;self.zoom_level=zoom_level.clamp(0.0,1.0);ifold_zoom_level!=self.zoom_level{self.is_dirty=true}}pubfnzooming_to(mutself,zoom_level: f32)-> Self{self.zoom_to(zoom_level);self}}
Run our application with cargo run and you may notice no difference at all, or you may notice that the zoom is now a bit smoother. These changes really only have an effect for big differences in frame rate between systems.
Smoothing Rotation + Movement
Next, let’s smooth our rotation and movement functions using the time delta between updates. Rotation and movement are already modeled as f32 values, so we just need to change our update function to scale the rotation/movement deltas using the time delta. See the full update function below, with smoothing for zoom, rotation, and movement.
implCameraTarget{pubfnupdate(mutscroll_evr: EventReader<MouseWheel>,keys: Res<Input<KeyCode>>,time: Res<Time>,mutcamera_query: Query<(&mutCameraTarget,&mutTransform),With<Camera>>,){let(muttarget,mutcamera_transform)=camera_query.single_mut();letdelta_y: f32=scroll_evr.read().map(|ev|ev.y).sum();letmutdelta_zoom_level: f32=ifdelta_y<0.0{-1.0}elseifdelta_y>0.0{1.0}else{0.0};ifkeys.pressed(KeyCode::Equals)||keys.pressed(KeyCode::Plus){delta_zoom_level=-1.0;}ifkeys.pressed(KeyCode::Minus){delta_zoom_level=1.0;}letmutdelta_rotation: f32=0.0;ifkeys.pressed(KeyCode::Q){delta_rotation=-1.0;}ifkeys.pressed(KeyCode::E){delta_rotation=1.0;}letmutdelta_x: f32=0.0;letmutdelta_z: f32=0.0;ifkeys.pressed(KeyCode::W)||keys.pressed(KeyCode::Up){delta_x=1.0;}ifkeys.pressed(KeyCode::S)||keys.pressed(KeyCode::Down){delta_x=-1.0;}ifkeys.pressed(KeyCode::D)||keys.pressed(KeyCode::Right){delta_z=-1.0;}ifkeys.pressed(KeyCode::A)||keys.pressed(KeyCode::Left){delta_z=1.0;}ifdelta_zoom_level!=0.0{target.change_zoom_to(delta_zoom_level*time.delta_seconds().clamp(0.0,1.0));}ifdelta_rotation!=0.0{target.change_rotation(delta_rotation*0.8*time.delta_seconds().clamp(0.0,1.0));}target.update_transform(&mutcamera_transform);ifdelta_x!=0.0||delta_z!=0.0{letmask=Vec3::new(1.0,1.0,1.0)-target.get_up();letlook_at=target.get_look_at()*mask;// multiply out the up component
letcamera_at=camera_transform.translation*mask;// multiply out the up component
letforward=(look_at-camera_at).normalize();letmutright_rotation=Transform::from_xyz(0.0,0.0,0.0);right_rotation.rotate_around(Vec3::default(),Quat::from_axis_angle(target.up,PI/2.0));letright=right_rotation*forward;letscale_factor=1000.0*time.delta_seconds().clamp(0.0,1.0);letnew_look_at=look_at+(forward*delta_x*scale_factor)+(right*delta_z*scale_factor);target.look_at(new_look_at);target.update_transform(&mutcamera_transform);}}...}
Run the application again using cargo run and you may or may not notice any differences.
Bounding Rectangle
The last improvement we want to make to our camera is to clamp it to a bounding rectangle. This will prevent players from moving the camera far away from the grid into no-man’s-land.
First, we need to add a BoundingBox to our CameraTarget struct. The BoundingBox will have an upper and lower position vector that we can use to clamp our look_at value. We’ll modify our CameraTarget implementation to account for the bounding box.
...implTextGem{...fnstartup(mutcommands: Commands,mutmeshes: ResMut<Assets<Mesh>>,mutgrid_materials: ResMut<Assets<ExtendedMaterial<StandardMaterial,GridMaterial>>>,){// Setup CameraTarget
letmutcamera_target=CameraTarget::default().looking_at(Vec3::new(0.0,0.0,0.0)).with_up(Vec3::Y).with_bounding_box(BoundingBox::new(Vec3::new(-3000.0,15.0,-3000.0),Vec3::new(3000.0,4000.0,3000.0),));...}}...#[derive(Debug, Clone, Copy, PartialEq)]pubstructBoundingBox{a: Vec3,b: Vec3,}implDefaultforBoundingBox{fndefault()-> Self{Self{a: Vec3::new(f32::MIN,f32::MIN,f32::MIN),b: Vec3::new(f32::MAX,f32::MAX,f32::MAX),}}}implBoundingBox{pubfnnew(a: Vec3,b: Vec3)-> Self{Self{a: a.min(b),b: a.max(b),}}pubfnclamp(&self,position: Vec3)-> Vec3{position.clamp(self.a,self.b)}}#[derive(Debug, Component)]pubstructCameraTarget{/// value in range [0.0, 1.0] which determines which zoom_level_offsets to use
zoom_level: f32,/// list of offsets to position the camera relative to the look_at point
zoom_level_offsets: Vec<Vec3>,/// point in space the camera should look at
look_at: Vec3,/// rotation angle around the up axis
rotation: f32,/// normal vector representing up for the camera
up: Vec3,/// bounding box for camera
bounding_box: BoundingBox,/// true wheh zoom_level, look_at, or up change
/// this let's our system know to update the camera transform in the scene
is_dirty: bool,}...implCameraTarget{...pubfnlook_at(&mutself,look_at: Vec3){letold_look_at=self.look_at;self.look_at=self.bounding_box.clamp(look_at);ifold_look_at!=self.look_at{self.is_dirty=true}}...pubfnset_bounding_box(&mutself,bounding_box: BoundingBox){letold_bounding_box=self.bounding_box;self.bounding_box=bounding_box;ifold_bounding_box!=self.bounding_box{self.is_dirty=true}}pubfnwith_bounding_box(mutself,bounding_box: BoundingBox)-> Self{self.set_bounding_box(bounding_box);self}...}
Run the application with cargo run and we’ll see that the player can no longer move the camera look_at position outside of the grid.
Next Up…
We’ve successfully made the camera work the ame independent of the frame rate, and we’ve also put bounds on it so the player can’t get too far away from the grid. We’re finally ready to start rendering some ASCII characters to the grid so we can start to represent our game objects. That’s what we’ll work on in the next post!
Full code for this post can be found here: TextGem Post 5.