有没有更好的方法将回调函数传递给Rust中的结构



我正在构建一个游戏引擎/CPU 3D渲染器,以学习渲染的基础知识。

我正在尝试组织代码,以便对其进行封装,并且我可以提供一个易于使用的公共API。

为此,我想提供这个engine.on_update(|&mut engine| {})方法,这样用户就可以传入自己的代码并在其中使用引擎。然后,这个回调将作为引擎更新循环的一部分执行。

这是现在的样子(我修改了代码,使其更容易,因为它已经很长了(

这是发动机模块。

use std::time::{Duration, Instant};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::PixelFormatEnum,
render::{Canvas, Texture, UpdateTextureError},
video::Window,
EventPump,
};
use crate::buffer::{ClearAuto, ClearColor, ColorBuffer, Drawable};
use crate::utils::NumOption;
pub struct EngineConfigParams {
pub window_title: Option<String>,
pub width: Option<usize>,
pub height: Option<usize>,
pub clear_color: Option<u32>,
pub fps: Option<u32>,
}
impl Default for EngineConfigParams {
fn default() -> Self {
EngineConfigParams {
window_title: None,
width: None,
height: None,
clear_color: None,
fps: None,
}
}
}
pub struct EngineConfig {
window_title: String,
width: usize,
height: usize,
clear_color: u32,
fps: u32,
}
impl EngineConfig {
pub fn new(params: EngineConfigParams) -> Self {
let default = EngineConfig::default();
EngineConfig {
window_title: params.window_title.unwrap_or(default.window_title),
width: params.width.unwrap_gt_or(0, default.width),
height: params.height.unwrap_gt_or(0, default.height),
clear_color: params.clear_color.unwrap_or(default.clear_color),
fps: params.fps.unwrap_gt_or(0, default.fps),
}
}
pub fn window_title(&self) -> &String {
&self.window_title
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn clear_color(&self) -> u32 {
self.clear_color
}
pub fn fps(&self) -> u32 {
self.fps
}
}
impl Default for EngineConfig {
fn default() -> Self {
EngineConfig {
window_title: "3D renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
fps: 60,
}
}
}
type EngineUpdateFn<'a> = &'a mut dyn FnMut(&mut EngineCore);
pub struct Engine<'a> {
core: EngineCore,
update: Option<EngineUpdateFn<'a>>,
previous_frame_time: Instant,
target_frame_time: Duration,
}
impl<'a> Engine<'a> {
pub fn build(mut config: EngineConfig) -> Engine<'a> {
let ctx = sdl2::init().unwrap();
let video = ctx.video().unwrap();
match video.display_mode(0, 0) {
Ok(mode) => {
config.width = mode.w as usize;
config.height = mode.h as usize;
println!("Display mode: {:?}", mode);
}
Err(e) => eprintln!(
"Failed to get display mode: {}, using default width and height",
e
),
};
let width = config.width;
let height = config.height;
let window = video
.window(&config.window_title, width as u32, height as u32)
.borderless()
.position_centered()
.fullscreen()
.build()
.unwrap();
let canvas = window.into_canvas().build().unwrap();
let color_buffer = ColorBuffer::new(width, height);
let event_pump = ctx.event_pump().unwrap();
println!("WindowCtx w: {} h: {}", width, height);
let fps = config.fps;
Engine {
core: EngineCore {
config,
canvas,
color_buffer,
event_pump,
},
update: None,
previous_frame_time: Instant::now(),
target_frame_time: Duration::new(0, 1_000_000_000u32 / fps),
}
}
pub fn config(&self) -> &EngineConfig {
&self.core.config
}
pub fn on_update(&mut self, f: EngineUpdateFn<'a>) {
self.update = Some(f);
self.update();
}
pub fn user_update(&mut self) {
self.update.as_mut().unwrap()(&mut self.core);
}
pub fn update(&mut self) {
self.target_frame_time = Duration::new(0, 1_000_000_000u32 / self.core.config.fps);
let texture_creator = self.core.canvas.texture_creator();
let mut texture = texture_creator
.create_texture(
PixelFormatEnum::ARGB8888,
sdl2::render::TextureAccess::Streaming,
self.core.config.width as u32,
self.core.config.height as u32,
)
.unwrap();
let mut running = true;
while running {
self.previous_frame_time = Instant::now();
running = self.core.process_input();
self.user_update();
self.core.render_buffer(&mut texture).unwrap();
self.core.clear();
let now = Instant::now();
let frame_time = now - self.previous_frame_time;
println!(
"Time this frame {}ms {} FPS",
frame_time.as_millis(),
1000u128 / frame_time.as_millis()
);
if frame_time.as_nanos() < self.target_frame_time.as_nanos() {
::std::thread::sleep(self.target_frame_time - frame_time);
}
}
}
}
pub struct EngineCore {
config: EngineConfig,
canvas: Canvas<Window>,
color_buffer: ColorBuffer,
event_pump: EventPump,
}
impl EngineCore {
fn process_input(&mut self) -> bool {
for event in self.event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
println!("Received quit event, shutting down");
return false;
}
_ => {}
}
}
true
}
fn render_buffer(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
self.copy_buffer_to_canvas(texture)?;
self.canvas.present();
Ok(())
}
fn copy_buffer_to_canvas(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
texture.update(None, self.color_buffer.pixels(), self.config.width * 4)?;
self.canvas.copy(texture, None, None).unwrap();
Ok(())
}
pub fn config(&self) -> &EngineConfig {
&self.config
}
}
impl ClearAuto for EngineCore {
fn clear(&mut self) {
self.color_buffer.clear(self.config.clear_color);
}
}
impl Drawable for EngineCore {
fn draw_grid(&mut self, spacing: usize, color: Option<u32>) {
self.color_buffer.draw_grid(spacing, color);
}
fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: u32) {
self.color_buffer.draw_rect(x, y, width, height, color);
}
fn draw_point(&mut self, x: usize, y: usize, color: u32) {
self.color_buffer.draw_point(x, y, color);
}
}

在main.rs中(再次修剪网格/点操作(

use renderer3d::prelude::*;
use vecx::Vec3;
pub fn main() {
let mut eng = Engine::build(EngineConfig {
window_title: "3d Renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
});
let points = build_cube();
let fov = 640.0;
let cam_pos = Vec3(0.0, 0.0, -5.0);
let mut rotation = Vec3(0.0, 0.0, 0.0);
println!("Start update");
eng.on_update(&mut |eng: &mut Engine| {
eng.draw_grid(10, Some(0xFF333333));
rotation = rotation + Vec3(0.01, 0.02, 0.0);
let mut points = transform_points(&points, rotation);
points = project_points(&points, cam_pos, fov);
points.iter().for_each(|point| {
let mut x = point.x();
let mut y = point.y();
x += eng.config().width as f64 / 2.0;
y += eng.config().height as f64 / 2.0;
eng.draw_rect(x as usize, y as usize, 4, 4, 0xFFFF0000);
});
});
}

正如您所看到的,这个回调可以从外部范围访问和变异变量,这一点很重要。

我经历了很多尝试和错误,最终找到了一些有效的方法,但事实上我必须执行unsafe代码,这让我怀疑这是否真的是正确的方法。

尤其是我担心我试图像在其他不太安全/限制性的语言中那样设计太多,并且我错过了如何在Rust中正确设计它。

更新

以下是应用已接受答案的解决方案后的完整最终代码

发动机模块

use std::time::{Duration, Instant};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::PixelFormatEnum,
render::{Canvas, Texture, UpdateTextureError},
video::Window,
EventPump,
};
use crate::buffer::{ClearAuto, ClearColor, ColorBuffer, Drawable};
use crate::utils::NumOption;
pub struct EngineConfigParams {
pub window_title: Option<String>,
pub width: Option<usize>,
pub height: Option<usize>,
pub clear_color: Option<u32>,
pub fps: Option<u32>,
}
impl Default for EngineConfigParams {
fn default() -> Self {
EngineConfigParams {
window_title: None,
width: None,
height: None,
clear_color: None,
fps: None,
}
}
}
pub struct EngineConfig {
window_title: String,
width: usize,
height: usize,
clear_color: u32,
fps: u32,
}
impl EngineConfig {
pub fn new(params: EngineConfigParams) -> Self {
let default = EngineConfig::default();
EngineConfig {
window_title: params.window_title.unwrap_or(default.window_title),
width: params.width.unwrap_gt_or(0, default.width),
height: params.height.unwrap_gt_or(0, default.height),
clear_color: params.clear_color.unwrap_or(default.clear_color),
fps: params.fps.unwrap_gt_or(0, default.fps),
}
}
pub fn window_title(&self) -> &String {
&self.window_title
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
}
impl Default for EngineConfig {
fn default() -> Self {
EngineConfig {
window_title: "3D renderer".to_string(),
width: 800,
height: 600,
clear_color: 0xFF000000,
fps: 60,
}
}
}
type EngineUpdateFn<'a> = &'a mut dyn FnMut(&mut EngineCore);
pub struct Engine<'a> {
core: EngineCore,
update: Option<EngineUpdateFn<'a>>,
previous_frame_time: Instant,
target_frame_time: Duration,
}
impl<'a> Engine<'a> {
pub fn build(mut config: EngineConfig) -> Engine<'a> {
let ctx = sdl2::init().unwrap();
let video = ctx.video().unwrap();
config = EngineConfig {
width: config.width,
height: config.height,
..EngineConfig::default()
};
match video.display_mode(0, 0) {
Ok(mode) => {
config.width = mode.w as usize;
config.height = mode.h as usize;
println!("Display mode: {:?}", mode);
}
Err(e) => eprintln!(
"Failed to get display mode: {}, using default width and height",
e
),
};
let width = config.width;
let height = config.height;
let window = video
.window(&config.window_title, width as u32, height as u32)
.borderless()
.position_centered()
.fullscreen()
.build()
.unwrap();
let canvas = window.into_canvas().build().unwrap();
let color_buffer = ColorBuffer::new(width, height);
let event_pump = ctx.event_pump().unwrap();
println!("WindowCtx w: {} h: {}", width, height);
let fps = config.fps;
Engine {
core: EngineCore {
config,
canvas,
color_buffer,
event_pump,
},
update: None,
previous_frame_time: Instant::now(),
target_frame_time: Duration::new(0, 1_000_000_000u32 / fps),
}
}
pub fn config(&self) -> &EngineConfig {
&self.core.config
}
pub fn on_update(&mut self, f: EngineUpdateFn<'a>) {
self.update = Some(f);
self.update();
}
pub fn user_update(&mut self) {
self.update.as_mut().unwrap()(&mut self.core);
}
pub fn update(&mut self) {
self.target_frame_time = Duration::new(0, 1_000_000_000u32 / self.core.config.fps);
let texture_creator = self.core.canvas.texture_creator();
let mut texture = texture_creator
.create_texture(
PixelFormatEnum::ARGB8888,
sdl2::render::TextureAccess::Streaming,
self.core.config.width as u32,
self.core.config.height as u32,
)
.unwrap();
let mut running = true;
while running {
self.previous_frame_time = Instant::now();
running = self.core.process_input();
self.user_update();
self.core.render_buffer(&mut texture).unwrap();
self.core.clear();
let now = Instant::now();
let frame_time = now - self.previous_frame_time;
println!(
"Time this frame {}ms {} FPS",
frame_time.as_millis(),
1000u128 / frame_time.as_millis()
);
if frame_time.as_nanos() < self.target_frame_time.as_nanos() {
::std::thread::sleep(self.target_frame_time - frame_time);
}
}
}
}
pub struct EngineCore {
config: EngineConfig,
canvas: Canvas<Window>,
color_buffer: ColorBuffer,
event_pump: EventPump,
}
impl EngineCore {
fn process_input(&mut self) -> bool {
for event in self.event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => {
println!("Received quit event, shutting down");
return false;
}
_ => {}
}
}
true
}
fn render_buffer(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
self.copy_buffer_to_canvas(texture)?;
self.canvas.present();
Ok(())
}
fn copy_buffer_to_canvas(&mut self, texture: &mut Texture) -> Result<(), UpdateTextureError> {
texture.update(None, self.color_buffer.pixels(), self.config.width * 4)?;
self.canvas.copy(texture, None, None).unwrap();
Ok(())
}
pub fn config(&self) -> &EngineConfig {
&self.config
}
}
impl ClearAuto for EngineCore {
fn clear(&mut self) {
self.color_buffer.clear(self.config.clear_color);
}
}
impl Drawable for EngineCore {
fn draw_grid(&mut self, spacing: usize, color: Option<u32>) {
self.color_buffer.draw_grid(spacing, color);
}
fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: u32) {
self.color_buffer.draw_rect(x, y, width, height, color);
}
fn draw_point(&mut self, x: usize, y: usize, color: u32) {
self.color_buffer.draw_point(x, y, color);
}
}

main.rs

use renderer3d::{prelude::*, Mesh};
use vecx::Vec3;
pub fn main() {
let mut eng = Engine::build(EngineConfig::new(EngineConfigParams {
window_title: Some("3d Renderer".to_string()),
..EngineConfigParams::default()
}));
let cube = Mesh::cube();
let fov = 640.0;
let cam_pos = Vec3(0.0, 0.0, -5.0);
let mut rotation = Vec3(0.0, 0.0, 0.0);
println!("Start update");
eng.on_update(&mut |eng| {
eng.draw_grid(10, Some(0xFF333333));
rotation = rotation + Vec3(0.01, 0.02, 0.0);
let mut points = cube
.face_vertices()
.map(|fv| transform_points(&fv, rotation))
.flatten()
.collect();
points = project_points(&points, cam_pos, fov);
points.iter().for_each(|point| {
let mut x = point.x();
let mut y = point.y();
x += eng.config().width() as f64 / 2.0;
y += eng.config().height() as f64 / 2.0;
eng.draw_rect(x as usize, y as usize, 4, 4, 0xFFFF0000);
});
});
}
fn transform_points(points: &Vec<Vec3>, rotation: Vec3) -> Vec<Vec3> {
points.iter().map(|p| p.rot(rotation)).collect()
}
fn project_points(points: &Vec<Vec3>, cam_pos: Vec3, fov: f64) -> Vec<Vec3> {
points
.iter()
.map(|point| {
let z = point.z() + cam_pos.z();
let mut x = point.x() / z;
let mut y = point.y() / z;
x *= fov;
y *= fov;
Vec3(x, y, z)
})
.collect()
}

问题不在于来自外部作用域的变量,而是回调具有对Engine的可变访问权限,但它本身来自engine,因此存在两个共存的可变引用。理论上,例如,它可以通过Engine中的数据指针和直接从不同线程使用捕获的变量来访问捕获的变量,从而导致数据竞争。

由于您可能不需要访问回调中的回调,所以我将Engine拆分为两个结构:一个包含回调,另一个包含update()on_update()方法,另一种包含所有其他内容。这样,回调将获得对内部结构的可变引用,它可以这样做——因为它不包含在内部结构中,但仍然能够调用所有引擎的函数。

最新更新