首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >生锈-向组件游戏中添加事件侦听器

生锈-向组件游戏中添加事件侦听器
EN

Stack Overflow用户
提问于 2019-08-18 19:45:07
回答 2查看 2.3K关注 0票数 2

我正在尝试在web程序集中创建一个游戏。我选择用铁锈来准备它,然后用货网编译它。我成功地获得了一个工作的游戏循环,但由于生锈的借用机制,我在添加MouseDownEvent侦听器时遇到了问题。我非常喜欢编写“安全”代码(不使用“不安全”关键字)。

此时,游戏简单地将一个红色方块从(0,0)移动到(700,500),速度取决于距离。我希望下一步使用用户,单击update目标。

这是游戏的简化和工作代码。

静态/index.html

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">

<head>
    <title>The Game!</title>
</head>

<body>
    <canvas id="canvas" width="600" height="600">
    <script src="game.js"></script>
</body>

</html>

src/main.rs

代码语言:javascript
复制
mod game;

use game::Game;

use stdweb::console;
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::CanvasRenderingContext2d;
use stdweb::web::html_element::CanvasElement;

use stdweb::web::event::MouseDownEvent;

fn main()
{
    let canvas: CanvasElement = document()
        .query_selector("#canvas")
        .unwrap()
        .unwrap()
        .try_into()
        .unwrap();

    canvas.set_width(800u32);
    canvas.set_height(600u32);


    let context = canvas.get_context().unwrap();

    let game: Game = Game::new();

    // canvas.add_event_listener(|event: MouseDownEvent|
    // {
    //     game.destination.x = (event.client_x() as f64);
    //     game.destination.y = (event.client_y() as f64);
    // });

    game_loop(game, context, 0f64);
}


fn game_loop(mut game : Game, context : CanvasRenderingContext2d, timestamp : f64)
{
    game.cycle(timestamp);
    draw(&game,&context);

    stdweb::web::window().request_animation_frame( |time : f64| { game_loop(game, context, time); } );
}


fn draw(game : &Game, context: &CanvasRenderingContext2d)
{
    context.clear_rect(0f64,0f64,800f64,800f64);
    context.set_fill_style_color("red");
    context.fill_rect(game.location.x, game.location.y, 5f64, 5f64);
}

src/game.rs

代码语言:javascript
复制
pub struct Point
{
    pub x: f64,
    pub y: f64,
}

pub struct Game
{
    pub time: f64,
    pub location: Point,
    pub destination: Point,
}

impl Game
{
    pub fn new() -> Game
    {
        let game = Game
        {   
            time: 0f64,
            location: Point{x: 0f64, y: 0f64},
            destination: Point{x: 700f64, y: 500f64},
        };

        return game;
    }

    pub fn cycle(&mut self, timestamp : f64)
    {
        if timestamp - self.time > 10f64
        {
            self.location.x += (self.destination.x - self.location.x) / 10f64; 
            self.location.y += (self.destination.y - self.location.y) / 10f64;

            self.time = timestamp;
        }
    }
}

main.rs中注释掉的部分是我添加MouseDownEvent侦听器的尝试。不幸的是,它会生成一个编译错误:

代码语言:javascript
复制
error[E0505]: cannot move out of `game` because it is borrowed
  --> src\main.rs:37:15
   |
31 |       canvas.add_event_listener(|event: MouseDownEvent|
   |       -                         ----------------------- borrow of `game` occurs here
   |  _____|
   | |
32 | |     {
33 | |         game.destination.x = (event.client_x() as f64);
   | |         ---- borrow occurs due to use in closure
34 | |         game.destination.y = (event.client_y() as f64);
35 | |     });
   | |______- argument requires that `game` is borrowed for `'static`
36 |
37 |       game_loop(game, context, 0f64);
   |                 ^^^^ move out of `game` occurs here

我非常想知道如何正确地实现一种读取用户输入到游戏中的方法。它不需要是异步的。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2019-08-19 12:11:42

在您的示例中,game_loop拥有game,因为它被移动到循环中。因此,任何应该改变游戏的事情都需要在game_loop内部发生。要将事件处理与此相结合,您有多个选项:

备选案文1

让为事件进行投票。

创建一个事件队列,您的game_loop将有一些逻辑来获取第一个事件并处理它。

您必须在这里处理同步问题,因此我建议您阅读互斥并发性的一般内容。但一旦你掌握了它的诀窍,这应该是一项相当容易的任务。您的循环获得一个引用,每个事件处理程序获得一个引用,所有这些都尝试解锁互斥锁,然后访问队列(向量可能)。

这将使您的game_loop成为他们所有的一个单一的真理,这是一个流行的引擎设计,因为它是很容易推理和开始。

但也许你不想那么集中。

选项2

让事件发生在循环之外

这个想法将是一个更大的重构。你可以把你的Game放在一个带有互斥锁的lazy_static里。

每次调用game_loop时,它都会尝试获取所述Mutex的锁,然后执行游戏计算。

当输入事件发生时,该事件还会尝试获取Game上的互斥对象。这意味着当game_loop正在处理时,没有处理输入事件,但是它们会试图在滴答之间进入。

这里的一个挑战是保持输入顺序,并确保输入处理得足够快。这可能是一个更大的挑战,以获得完全正确。但是这个设计会给你一些可能性。

这个想法的一个充实的版本是Amethyst,它是大规模并行的,并且是一个干净的设计。但他们在引擎背后采用了更为复杂的设计。

票数 1
EN

Stack Overflow用户

发布于 2019-08-19 13:03:06

在这种情况下,我认为编译器错误消息是非常清楚的。您正在尝试在闭包中借用game,用于'static生存期,然后您还试图移动game。是不允许的。我建议再读一遍Rust编程语言的书。专注于第四章--理解所有权。

为了缩短时间,你的问题归结为--如何分享一个可以变异的状态。实现这一目标有很多方法,但它确实取决于您的需求(单线程或多线程等)。我将使用Rc & RefCell来解决这个问题。

Rc (std:rc):

类型Rc<T>提供了在堆中分配的T类型值的共享所有权。在clone上调用Rc会产生一个指向堆中相同值的新指针。当指向给定值的最后一个Rc指针被销毁时,指向值也被销毁。

RefCell (std::牢房):

Cell<T>RefCell<T>类型的值可以通过共享引用(即公共&T类型)进行变异,而大多数锈蚀类型只能通过唯一(&mut T)引用进行变异。我们认为Cell<T>RefCell<T>提供了“内部可变性”,这与典型的呈现“继承可变性”的锈蚀类型形成了对比。

我对你的结构做了些什么:

代码语言:javascript
复制
struct Inner {
    time: f64,
    location: Point,
    destination: Point,
}

#[derive(Clone)]
pub struct Game {
    inner: Rc<RefCell<Inner>>,
}

这意味着什么?Inner保存游戏状态(与旧Game相同的字段)。新Game只有一个字段inner,它包含共享状态。

  • Rc<T> (T is RefCell<Inner>在本例中)-允许我多次克隆inner,但它不会克隆T
  • RefCell<T> (本例中TInner )-允许我永久地或可变地借用T,检查是在运行时完成的。

我现在可以多次克隆Game结构,它不会克隆RefCell<Inner>,只克隆Game & Rc。这是enclose!宏在更新的main.rs中所做的事情。

代码语言:javascript
复制
let game: Game = Game::default();

canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
    game.set_destination(event);
}));

game_loop(game, context, 0.);

没有enclose!宏:

代码语言:javascript
复制
let game: Game = Game::default();

// game_for_mouse_down_event_closure holds the reference to the
// same `RefCell<Inner>` as the initial `game`
let game_for_mouse_down_event_closure = game.clone();
canvas.add_event_listener(move |event: MouseDownEvent| {
    game_for_mouse_down_event_closure.set_destination(event);
});

game_loop(game, context, 0.);

更新的game.rs

代码语言:javascript
复制
use std::{cell::RefCell, rc::Rc};

use stdweb::traits::IMouseEvent;
use stdweb::web::event::MouseDownEvent;

#[derive(Clone, Copy)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

impl From<MouseDownEvent> for Point {
    fn from(e: MouseDownEvent) -> Self {
        Self {
            x: e.client_x() as f64,
            y: e.client_y() as f64,
        }
    }
}

struct Inner {
    time: f64,
    location: Point,
    destination: Point,
}

impl Default for Inner {
    fn default() -> Self {
        Inner {
            time: 0.,
            location: Point { x: 0., y: 0. },
            destination: Point { x: 700., y: 500. },
        }
    }
}

#[derive(Clone)]
pub struct Game {
    inner: Rc<RefCell<Inner>>,
}

impl Default for Game {
    fn default() -> Self {
        Game {
            inner: Rc::new(RefCell::new(Inner::default())),
        }
    }
}

impl Game {
    pub fn update(&self, timestamp: f64) {
        let mut inner = self.inner.borrow_mut();

        if timestamp - inner.time > 10f64 {
            inner.location.x += (inner.destination.x - inner.location.x) / 10f64;
            inner.location.y += (inner.destination.y - inner.location.y) / 10f64;

            inner.time = timestamp;
        }
    }

    pub fn set_destination<T: Into<Point>>(&self, location: T) {
        let mut inner = self.inner.borrow_mut();
        inner.destination = location.into();
    }

    pub fn location(&self) -> Point {
        self.inner.borrow().location
    }
}

更新的main.rs

代码语言:javascript
复制
use stdweb::traits::*;
use stdweb::unstable::TryInto;
use stdweb::web::document;
use stdweb::web::event::MouseDownEvent;
use stdweb::web::html_element::CanvasElement;
use stdweb::web::CanvasRenderingContext2d;

use game::Game;

mod game;

// https://github.com/koute/stdweb/blob/master/examples/todomvc/src/main.rs#L31-L39
macro_rules! enclose {
    ( ($( $x:ident ),*) $y:expr ) => {
        {
            $(let $x = $x.clone();)*
            $y
        }
    };
}

fn game_loop(game: Game, context: CanvasRenderingContext2d, timestamp: f64) {
    game.update(timestamp);
    draw(&game, &context);

    stdweb::web::window().request_animation_frame(|time: f64| {
        game_loop(game, context, time);
    });
}

fn draw(game: &Game, context: &CanvasRenderingContext2d) {
    context.clear_rect(0., 0., 800., 800.);
    context.set_fill_style_color("red");

    let location = game.location();
    context.fill_rect(location.x, location.y, 5., 5.);
}

fn main() {
    let canvas: CanvasElement = document()
        .query_selector("#canvas")
        .unwrap()
        .unwrap()
        .try_into()
        .unwrap();

    canvas.set_width(800);
    canvas.set_height(600);

    let context = canvas.get_context().unwrap();

    let game: Game = Game::default();

    canvas.add_event_listener(enclose!( (game) move |event: MouseDownEvent| {
        game.set_destination(event);
    }));

    game_loop(game, context, 0.);
}

请在将来分享任何代码之前,安装并使用铁锈

票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/57547849

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档