使用Rust编写推箱子游戏基础篇

使用Rust编写推箱子游戏教程已翻译完成 项目地址 在线阅读  

推箱子是个啥样的游戏嘞?

没玩过推箱子游戏?想当年用诺基亚黑白屏手机的时候就有这款游戏了。你可以下载一个玩一下或者点这里看下*的介绍。本教程就是教大家怎么使用Rust和现有的游戏引擎、素材,编写一个可以玩的推箱子游戏。

为什么要使用Rust编写推箱子游戏呢?

我是2019年3月份开始学习Rust的,在编写本教程前我就使用Rust开发过游戏。在学习和使用Rust的过程中我还写了一些博客,感觉从Rust游戏开发中我学到了很多,于是乎我就有个想法:这么好的东西得分享给大家啊,让大家都来体验下啊,独乐乐不如众乐乐!然后就有了本教程。

那是不是得先去学习下Rust呢?

不需要。本教程会手把手一步一步教你怎么使用Rust编写游戏,也会对一些Rust的语法进行一些必要的解释。对于一些知识点我们也会提供更详细的介绍链接供您学习参考。当然本教程主要是通过编写一个有趣的游戏顺便对Rust语言进行简单的介绍,所以有些Rust的知识点我们可能不会也没必要过多的深入。

项目搭建

建议使用rustup安装管理Rust。安装好Rust后可以在命令行输入以下俩条命令,检查确认是否安装成功:

$ rustc --version 
rustc 1.40.0

$ cargo --version 
cargo 1.40.0

输出的版本信息未必都是这样的,但建议使用比较新的Rust版本。

创建项目

Cargo是Rust的包管理工具,可以使用它创建我们的游戏项目。首先切换到游戏项目存储路径,然后再输入以下命令:

$ cargo init rust-sokoban

命令执行成功后,会在当前目录下创建一个名称为rust-sokoban的文件夹。文件夹内部是这个样子的:

├── src

│ └── main.rs

└── Cargo.toml

切换到文件夹rust-sokoban并运行命令 cargo run ,你会看到类似下面的输出信息:

$ cargo run

Compiling rust-sokoban v0.1.0

Finished dev [unoptimized + debuginfo] target(s) in 1.30s

Running `../rust-sokoban/target/debug/rust-sokoban`

Hello, world!

添加游戏开发依赖

接下来让我们一起把默认生成的项目修改成一个游戏项目! 我们使用当前最受欢迎的2D游戏引擎之一的ggez

还记得我们刚才在项目目录里看到的Cargo.toml文件吧?这个文件是用来管理项目依赖的,所以需要把我们需要使用到的crate添加到这个文件中。就像这样添加 ggez 依赖:

[dependencies] ggez = "0.5.1"

MORE: 更多关于Cargo.toml的信息可以看 这里.

接下来再次执行cargo run.这次执行的会长一点,因为需要从crates.io下载我们配置的依赖库并编译链接到我们库中。

cargo run

Updating crates.io index Downloaded .... ....

Compiling .... ....

Finished dev [unoptimized + debuginfo] target(s) in 2m 15s

Running `.../rust-sokoban/target/debug/rust-sokoban`

Hello, world!

NOTE: 如果你是使用的Ubuntu操作系统,在执行命令的时候可能会报错,如果报错信息有提到alsa 和libudev可以通过执行下面的命令安装解决: sudo apt-get install libudev-dev libasound2-dev.

接下来我们在main.rs文件中使用ggez创建一个窗口。只是创建一个空的窗口,代码比较简单:

use ggez::{conf, event, Context, GameResult};
use std::path;

// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {}

// This is the main event loop. ggez tells us to implement
// two things:
// - updating
// - rendering
impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }

    fn draw(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update draw here
        Ok(())
    }
}

pub fn main() -> GameResult {
    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = &mut context_builder.build()?;
    // Create the game state
    let game = &mut Game {};
    // Run the main event loop
    event::run(context, event_loop, game)
}

可以把代码复制到main.rs文件中,并再次执行cargo run,你会看到:

使用Rust编写推箱子游戏基础篇

基本概念和语法

现在我们有了个窗口,我们创建了个窗口耶!接下来我们一起分析下代码并解释下使用到的Rust概念和语法。

引入

您应该在其它编程语言中也接触过这个概念,就是把我们需要用到的依赖包(或crate)里的类型和命名空间引入到当前的代码作用域中。在Rust中,使用use实现引入功能:

// 从ggez命名空间引入conf, event, Context 和 GameResult

use ggez::{conf, event, Context, GameResult};

结构体声明

// This struct will hold all our game state

// For now there is nothing to be held, but we'll add

// things shortly.

struct Game {}

MORE: 查看更多结构体相关信息可以点 这里.

实现特征

特征类似其它语言中的接口,就是用来表示具备某些行为的特定类型。在我们的示例中需要结构体Game实现EventHandler特征。

// This is the main event loop. ggez tells us to implement
// two things:
// - updating
// - rendering
impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }

    fn draw(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update draw here
        Ok(())
    }
}

MORE: 想更深入的了解特征可以点 这里.

函数

我们还需要学习下怎么使用Rust编写函数:

    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }

你可能会疑惑这里的self是几个意思呢?这里使用self代表函数update是属于结构体的实例化对象而不是静态的。

MORE: 想深入了解函数可以点 这里.

可变语法

你可能更疑惑&mut self这里的&mut是做什么的? 这个主要用来声明一个对象(比如这里的self)是否可以被改变的。再来看个例子:

let a = 10; // a是不可变的,因为没有使用`mut`声明它是可变的
let mut b = 20; // b是可变的,因为使用了`mut`声明了它是可变的

再回头看update函数,我们使用了&mut 声明self是实例对象的可变引用。有没有点感觉了, 要不我们再看一个例子:

// 一个简单的结构体X
struct X {
    num: u32
}

//结构体X的实现代码块
impl X {
    fn a(&self) { self.num = 5 } 
    // 在函数a中不能修改`self`,这会编译失败的,因为是使用的`&self`

    fn b(&mut self) { self.num = 5 } 
    // 在函数b中可以修改`self`,因为使用的是`&mut self`
}

MORE: 想更多的了解可变性可以看 这里 (虽然是使用的Java作为演示语言讲解的,但对于理解可变性还是很有帮助地), 另外还可以看 这里.

对代码和Rust语法的简单介绍就先到这里,让我们继续前进吧,下一节见!

实体构建系统

在本章节中我们将更详细的介绍下推箱子游戏并探讨下该怎么构建我们的游戏

推箱子游戏

如果你还没玩过推箱子游戏,可以先看下这张推箱子游戏的动态图片:

使用Rust编写推箱子游戏基础篇

游戏中有墙有箱子,玩家的目标是把箱子推到它们的位置上。

ECS

ECS (实体构建系统)是一种遵循组合优于继承的构建游戏的模式. 像多少Rust游戏一样,我们编写的推样子游戏也会大量使用ECS,所以我们有必要先花点时间熟悉下ECS

  • 组件(Components) - 组件只包含数据不包含行为,比如:位置组件、可渲染组件和运动组件。
  • 实体(Entities) - 实体是由多个组件组成的,比如玩家,可能是由位置组件、可渲染组件、动作组件组合而成的,而地板可能只需要位置组件和可渲染组件,因为它不会动。也可以说实体几乎就是包含一个或多个具有唯一标示信息的组件的容器。
  • 系统(Systems) - 系统使用实体和组件并包含基于数据的行为和逻辑。比如渲染系统:它可以一个一个的处理并绘制可渲染实体。就像我们上面提到的组件本身不包含行为,而是通过系统根据数据创建行为。

如果现在觉得还不是很理解ECS,也没有关系,我们下面章节还会介绍一些结合推箱子游戏的实例。

推箱子游戏结构

根据我们对推箱子游戏的了解,要编写一个这样的游戏,起码要有:墙、玩家、地板、箱子还有方块斑点这些实体。

接下来我们需要确认下怎么创建实体,也就是需要什么样的组件。首先,我们需要跟踪地图上所有的东西,所以我们需要一些位置组件。其次,某些实体可以移动,比如:玩家和箱子。所以我们需要一些动作组件。最后,我们还需要绘制实体,所以还需要一些渲染组件。

按照这个思路我们先出一版:

  1. 玩家实体: 有位置组件可渲染组件运动组件组成
  2. 墙实体: 有位置组件可渲染组件组成
  3. 地板实体: 有位置组件可渲染组件组成
  4. 箱子实体: 有位置组件可渲染组件运动组件组成
  5. 方框斑点组件: 有位置组件运动组件组成

第一次接触ECS是有点难于理解,如果不理解这些也没关系,可接着往下面看。

Specs

最后我们需要一个提供ECScrate,虽然这样的库有一大把,但在本教程中我们使用 specs ,需要在Cargo.toml文件中配置specs依赖:

[dependencies]
ggez = "0.5.1"
specs = { version = "0.15.0", features = ["specs-derive"] }

加油!接下来我们就开始编写组件和实体了!是不是很期待?

CODELINK: 可以点 这里获取本章节完整代码.

组件和实体

嗨,少年!看你骨骼惊奇接下来就开始一起学习怎么结合specs创建、注册组件和实体。

定义组件

我们先从定义组件开始。先前我们提到过位置组件可渲染组件动作组件(这个后面再讲哈)。我们需要用一些组件标识实体,比如可以让一个实体包含墙组件标识它是墙。

可以直接简单的说:位置组件其实就是用来存储地图坐标的x、y、z值的可以用来定位;渲染组件就是使用字符串存储一个需要绘制的图片的路径;另外一些组件基本都是 标记型组件并不存储数据。

#![allow(unused)]
fn main() {
// Components
#[derive(Debug, Component, Clone, Copy)]
#[storage(VecStorage)]
pub struct Position {
    x: u8,
    y: u8,
    z: u8,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Renderable {
    path: String,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Wall {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Player {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Box {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct BoxSpot {}
}

#[storage(VecStorage)]这原来没见过是不是? 恭喜你,少年!你已经使用到了一个Rust很强大的功能过程宏。这种宏是一些可以在代码编译时对代码进行处理并生成新代码的特殊函数。

MORE: 如果你想更深入的了解宏,可以看 这里.

注册组件

specs中使用组件前需要先注册组件,就像这样:

把组件注册到world

#![allow(unused)]
fn main() {
// Register components with the world
pub fn register_components(world: &mut World) {
    world.register::<Position>();
    world.register::<Renderable>();
    world.register::<Player>();
    world.register::<Wall>();
    world.register::<Box>();
    world.register::<BoxSpot>();
}
}

创建实体

实体就是代表一系列组件,所以我们创建实体的方法就是简单地指定它们包含哪些组件。就像这个样子:

 
#![allow(unused)]
fn main() {
// Create a wall entity
pub fn create_wall(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/wall.png".to_string(),
        })
        .with(Wall {})
        .build();
}

pub fn create_floor(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 5, ..position })
        .with(Renderable {
            path: "/images/floor.png".to_string(),
        })
        .build();
}

pub fn create_box(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/box.png".to_string(),
        })
        .with(Box {})
        .build();
}

pub fn create_box_spot(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 9, ..position })
        .with(Renderable {
            path: "/images/box_spot.png".to_string(),
        })
        .with(BoxSpot {})
        .build();
}

pub fn create_player(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/player.png".to_string(),
        })
        .with(Player {})
        .build();
}
}


素材

睿智如你应该已经注意到了,我们还引用了些用于创建实体的素材,就是图片什么的。当然你要是觉得我们准备的素材不好看,也可以使用自己的素材。我们准备的素材就在下面了,你可以右键另存为下载到电脑上:

使用Rust编写推箱子游戏基础篇 使用Rust编写推箱子游戏基础篇 使用Rust编写推箱子游戏基础篇 使用Rust编写推箱子游戏基础篇 使用Rust编写推箱子游戏基础篇

接下来把这些图片放到我们的项目中。在项目目录中新建resources目录,用于存放项目需要用到的资源,目前我们只有图片资源需要存储,以后还会有配置文件啊,音频文件(第三章的第3小节会用到)啊什么的。为了区分不同的资源文件,在resources目录下再新建一个images目录,用于存放我们的png图片。你也可以按照自己的喜欢命名目录,除了只要你开心就好,还要记得在代码中引用这些资源时要写出正确的路径。一波操作下来后,我们项目的目录结构大概是这个样子地:

├── resources
│   └── images
│       ├── box.png
│       ├── box_spot.png
│       ├── floor.png
│       ├── player.png
│       └── wall.png
├── src
│   └── main.rs
└── Cargo.toml

创建游戏世界(World)

最后,当然只是本小节的最后,接下来在main函数的第一行就创建一个specs::World对象,把先前创建的实体还有素材都整合到一起。

use ggez;
use ggez::graphics;
use ggez::graphics::DrawParam;
use ggez::graphics::Image;
use ggez::nalgebra as na;
use ggez::{conf, event, Context, GameResult};
use specs::{
    join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt,
};

use std::path;

// Components
#[derive(Debug, Component, Clone, Copy)]
#[storage(VecStorage)]
pub struct Position {
    x: u8,
    y: u8,
    z: u8,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Renderable {
    path: String,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Wall {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Player {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Box {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct BoxSpot {}

// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
    world: World,
}

impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        Ok(())
    }
}

// Register components with the world
pub fn register_components(world: &mut World) {
    world.register::<Position>();
    world.register::<Renderable>();
    world.register::<Player>();
    world.register::<Wall>();
    world.register::<Box>();
    world.register::<BoxSpot>();
}

// Create a wall entity
pub fn create_wall(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/wall.png".to_string(),
        })
        .with(Wall {})
        .build();
}

pub fn create_floor(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 5, ..position })
        .with(Renderable {
            path: "/images/floor.png".to_string(),
        })
        .build();
}

pub fn create_box(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/box.png".to_string(),
        })
        .with(Box {})
        .build();
}

pub fn create_box_spot(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 9, ..position })
        .with(Renderable {
            path: "/images/box_spot.png".to_string(),
        })
        .with(BoxSpot {})
        .build();
}

pub fn create_player(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/player.png".to_string(),
        })
        .with(Player {})
        .build();
}

pub fn main() -> GameResult {
    let mut world = World::new();
    register_components(&mut world);

    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = &mut context_builder.build()?;

    // Create the game state
    let game = &mut Game { world };
    // Run the main event loop
    event::run(context, event_loop, game)
}

然后你就可以执行cargo run运行看下效果,当你满怀期待却发现看到的依然是一个空白的窗口,控制台里可能还多了些警告信息。这是因为我们还没有编写渲染的代码也就是还没有绘制这些实体。少侠,莫急!下一节,我们就开始绘制。到时这些因为引入而没有使用的警告也就自然消失了。

CODELINK: 你可以在 这里找到本小节完整的代码.

渲染系统

是时候开始创建第一个系统(system)了——渲染系统。这个系统负责把实体绘制到屏幕上,也就是能不能在窗口上看见点东西就看它的了。

渲染系统走起

首先我们定义个结构体RenderingSystem,它需要使用ggez的上下文对象(context)绘制实体。

#![allow(unused)]
fn main() {
pub struct RenderingSystem<'a> {
    context: &'a mut Context,
}
}

注意代码中的'a, ' 可不是单引号哦,在你的键盘上应该也是Esc键下面的那个建。这是什么东东,为何写法如此奇怪嘞?这是Rust的生命周期声明语法。因为Rust编译器自己推断不出结构体RenderingSystem持有的Context引用的有效性,所以需要我们使用生命周期声明语法告诉它。

MORE: 更深入的了解生命周期请点 这里.

接下来我们需要为结构体RenderingSystem实现System特征。当前只是编写个架子,并不对方法做具体实现。

#![allow(unused)]
fn main() {
// System implementation
impl<'a> System<'a> for RenderingSystem<'a> {
    // Data
    type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);

    fn run(&mut self, data: Self::SystemData) {
        let (positions, renderables) = data;
        // implementation here
    }
}
}

代码中定义的SystemData类型是方便访问位置和可渲染存储信息的。我们使用了只读存储ReadStorage,也就是只读取数据不修改数据。

最后在绘制循环中运行渲染系统。也就是当每次游戏更新时也同时根据实体的最新状态重新绘制实体。

#![allow(unused)]
fn main() {
impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            let mut rs = RenderingSystem { context };
            rs.run_now(&self.world);
        }

        Ok(())
    }
}
}

现在我们的代码是可以编译运行的,但是依然看不到任何东西,因为我们还没编写渲染的逻辑代码,也还没创建实体。

实现渲染系统

实现渲染系统需要做这些事:

  • 清空屏幕(确保不显示过去的)
  • 获取所有具备可渲染组件的实体,并按空间z轴排列好后渲染。这样可以保证实体可以一层一层累加渲染,比如玩家应该在地板上面,不然我们就看不到他了。
  • 按排列好的顺序一个一个的把实体渲染为图片展示。
  • 最后就可以在屏幕上看到它们了。
#![allow(unused)]
fn main() {
    fn run(&mut self, data: Self::SystemData) {
        let (positions, renderables) = data;

        // Clearing the screen (this gives us the backround colour)
        graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0));

        // Get all the renderables with their positions and sort by the position z
        // This will allow us to have entities layered visually.
        let mut rendering_data = (&positions, &renderables).join().collect::<Vec<_>>();
        rendering_data.sort_by_key(|&k| k.0.z);

        // Iterate through all pairs of positions & renderables, load the image
        // and draw it at the specified position.
        for (position, renderable) in rendering_data.iter() {
            // Load the image
            let image = Image::new(self.context, renderable.path.clone()).expect("expected image");
            let x = position.x as f32 * TILE_WIDTH;
            let y = position.y as f32 * TILE_WIDTH;

            // draw
            let draw_params = DrawParam::new().dest(na::Point2::new(x, y));
            graphics::draw(self.context, &image, draw_params).expect("expected render");
        }

        // Finally, present the context, this will actually display everything
        // on the screen.
        graphics::present(self.context).expect("expected to present");
    }
}

添加实体测试下

接下来我们创建一些用来测试的实体,验证下我们的代码是不是可以正常工作。

#![allow(unused)]
fn main() {
pub fn initialize_level(world: &mut World) {
    create_player(
        world,
        Position {
            x: 0,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_wall(
        world,
        Position {
            x: 1,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_box(
        world,
        Position {
            x: 2,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
}
}


最后我们把所有这些都整合到一起,然后编译运行,你会看到:

使用Rust编写推箱子游戏基础篇

是不是小激动?这是我们第一次实现了个渲染系统在窗口上绘制出了点东西。小激动一下就可以了,毕竟现在还只是显示了些静态的图片还不能称之为游戏,后面我们会让它更像个游戏。

最终的代码是这个样子的:

注意: 当前实现的渲染系统还比较简单,随着实体的增多可能会有性能问题。在第三章的批量渲染章节我们还会做些优化,敬请期待!

use ggez;
use ggez::graphics;
use ggez::graphics::DrawParam;
use ggez::graphics::Image;
use ggez::nalgebra as na;
use ggez::{conf, event, Context, GameResult};
use specs::{
    join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt,
};

use std::path;

const TILE_WIDTH: f32 = 32.0;

// Components
#[derive(Debug, Component, Clone, Copy)]
#[storage(VecStorage)]
pub struct Position {
    x: u8,
    y: u8,
    z: u8,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Renderable {
    path: String,
}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Wall {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Player {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct Box {}

#[derive(Component)]
#[storage(VecStorage)]
pub struct BoxSpot {}

// Systems
pub struct RenderingSystem<'a> {
    context: &'a mut Context,
}

// System implementation
impl<'a> System<'a> for RenderingSystem<'a> {
    // Data
    type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);

    fn run(&mut self, data: Self::SystemData) {
        let (positions, renderables) = data;

        // Clearing the screen (this gives us the backround colour)
        graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0));

        // Get all the renderables with their positions and sort by the position z
        // This will allow us to have entities layered visually.
        let mut rendering_data = (&positions, &renderables).join().collect::<Vec<_>>();
        rendering_data.sort_by_key(|&k| k.0.z);

        // Iterate through all pairs of positions & renderables, load the image
        // and draw it at the specified position.
        for (position, renderable) in rendering_data.iter() {
            // Load the image
            let image = Image::new(self.context, renderable.path.clone()).expect("expected image");
            let x = position.x as f32 * TILE_WIDTH;
            let y = position.y as f32 * TILE_WIDTH;

            // draw
            let draw_params = DrawParam::new().dest(na::Point2::new(x, y));
            graphics::draw(self.context, &image, draw_params).expect("expected render");
        }

        // Finally, present the context, this will actually display everything
        // on the screen.
        graphics::present(self.context).expect("expected to present");
    }
}

// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
    world: World,
}

// This is the main event loop. ggez tells us to implement
// two things:
// - updating
// - rendering
impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            let mut rs = RenderingSystem { context };
            rs.run_now(&self.world);
        }

        Ok(())
    }
}

// Register components with the world
pub fn register_components(world: &mut World) {
    world.register::<Position>();
    world.register::<Renderable>();
    world.register::<Player>();
    world.register::<Wall>();
    world.register::<Box>();
    world.register::<BoxSpot>();
}

// Create a wall entity
pub fn create_wall(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/wall.png".to_string(),
        })
        .with(Wall {})
        .build();
}

pub fn create_floor(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 5, ..position })
        .with(Renderable {
            path: "/images/floor.png".to_string(),
        })
        .build();
}

pub fn create_box(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/box.png".to_string(),
        })
        .with(Box {})
        .build();
}

pub fn create_box_spot(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 9, ..position })
        .with(Renderable {
            path: "/images/box_spot.png".to_string(),
        })
        .with(BoxSpot {})
        .build();
}

pub fn create_player(world: &mut World, position: Position) {
    world
        .create_entity()
        .with(Position { z: 10, ..position })
        .with(Renderable {
            path: "/images/player.png".to_string(),
        })
        .with(Player {})
        .build();
}

// Initialize the level
pub fn initialize_level(world: &mut World) {
    create_player(
        world,
        Position {
            x: 0,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_wall(
        world,
        Position {
            x: 1,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_box(
        world,
        Position {
            x: 2,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
}

pub fn main() -> GameResult {
    let mut world = World::new();
    register_components(&mut world);
    initialize_level(&mut world);

    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = &mut context_builder.build()?;

    // Create the game state
    let game = &mut Game { world };
    // Run the main event loop
    event::run(context, event_loop, game)
}

CODELINK: 可以点 这里获取本章节完整代码.


第一章完...也可以访问在线阅读地址继续观看


上一篇:Github ssh 多账号配置


下一篇:shell分发公钥到目标服务器,实现免密登录