ggez游戏开发入门
内容来自github,介绍了ggez的基本使用和框架。还是比较简单明了的,就是有一个state保存游戏状态,以及update和draw两个方法函数。
完整代码如下:
use ggez::*;
struct State {
dt: std::time::Duration,
}
impl ggez::event::EventHandler<GameError> for State {
fn update(&mut self, ctx: &mut Context) -> GameResult {
self.dt = timer::delta(ctx);
Ok(())
}
fn draw(&mut self, _ctx: &mut Context) -> GameResult {
println!("Hello ggez! dt = {}ns", self.dt.as_nanos());
Ok(())
}
}
fn main() {
let state = State {
dt: std::time::Duration::new(0, 0),
};
let c = conf::Conf::new();
let (ctx, event_loop) = ContextBuilder::new("hello_ggez", "awesome_person")
.default_conf(c)
.build()
.unwrap();
event::run(ctx, event_loop, state);
}
👋 Hello ggez
!
This is a guide to create a program that prints Hello ggez! dt = 78986ns
every frame.
At the end of this guide you will:
- Have created a simple program using
ggez
- Know the basic scaffolding of a
ggez
program
⚠ Prerequisites
Rust
Rust will need to be installed. Follow Rust's guides for installing the latest stable version. You did know this was a Rust library right? 😉
🛠 Project Setup
Cargo Crate
Open up your terminal and make sure it is in the location where you want to create your new project folder.
cargo new --bin hello_ggez
cd hello_ggez
Open up Cargo.toml
with a text editor and add ggez
as a dependency:
[package]
name = "hello_ggez"
version = "0.1.0"
# Replace with your name and email
authors = ["Awesome Person [email protected]"]
[dependencies]
ggez = "0.6"
✔ Check Project Setup
Test to make sure everything is correct by running cargo run
.
Be patient.
This can take a while on the first run.
Subsequent builds will be much faster.
Be sure it says Hello, world!
at the bottom before continuing.
ggez
We have successfully compiled ggez
, but we haven't used it yet!
What we have currently is the canonical Rust hello world.
Let's change that and create a more appropriate hello world for ggez
.
It is why we're here after all.
First we'll tell Rust we want to use ggez
.
Add this to the top of your src/main.rs
:
use ggez::*;
Now we're cooking with ggez
.
Loop
All games have a loop. Often the loop is referred to as a game loop.
Game loops are responsible for:
- Handling events such as keyboard, mouse, closing window, etc.
- Updating state such as player position, health, etc.
- Drawing shapes, images, etc.
ggez
provides an EventHandler
as the default recommended interface to its internal loop to use in our games. Thanks ggez
!
You might have noticed EventHandler
is a Rust Trait.
This means it is intended to be implemented on a struct.
There are quite a few callbacks defined on the Trait, but only 2 are required: update and draw.
Let's add EventHandler
to our src/main.rs
file:
struct State {}
impl ggez::event::EventHandler<GameError> for State {
fn update(&mut self, ctx: &mut Context) -> GameResult {
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult {
Ok(())
}
}
You'll notice a couple new things here: State
, Context
, and GameResult
.
State
is all of the data and information required to represent our game's current state.
These could be player positions, scores, cards in your hand, etc..
What is included in your state is very dependent on the game you are making.
Context
is how ggez
gives you access to hardware such as mouse, keyboard, timers, graphics, sound, etc..
GameResult
is a utility provided by ggez
to signify if there was an error or not.
Internally it's just a Result<(), GameError>
, which is why we implement EventHandler
with GameError
, so that on_error
knows what to expect.
But, we're not going to write any bugs right? 😉
In your main, you will need to create an instance of State
.
pub fn main() {
let state = State {};
}
✔ Check Loop Setup
Test to make sure everything is correct by running cargo run
.
You should see this:
warning: unused variable: `state`
--> src\main.rs:15:9
|
15 | let state = &mut State {};
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_state`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `ctx`
--> src\main.rs:6:26
|
6 | fn update(&mut self, ctx: &mut Context) -> GameResult {
| ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
warning: unused variable: `ctx`
--> src\main.rs:9:24
|
9 | fn draw(&mut self, ctx: &mut Context) -> GameResult {
| ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
And then nothing should happen after it runs. If you saw the warnings and nothing happened, you can continue.
Context
Although we have created a loop, we haven't actually started using it yet. Also, don't mind the loop does nothing right now. We'll get to that soon.
Now is the time for us to interface with our hardware and do something fun.
To do that, you need to create a Context
courtesy of ggez
.
Add this to the end of your main
fn:
let c = conf::Conf::new();
let (ctx, event_loop) = ContextBuilder::new("hello_ggez", "awesome_person")
.default_conf(c)
.build()
.unwrap();
This will create a Context
with the game_id
hello_ggez
and the author awesome_person
.
It will also create an EventsLoop
.
We'll need it in a minute to call run
.
Feel free to replace the author with yourself.
You are awesome after all.
Now you're ready to kick off the loop!
event::run(ctx, event_loop, state);
✔ Check Context
Once again run cargo run
You should get 2 warnings:
warning: unused variable: `ctx`
--> src\main.rs:6:26
|
6 | fn update(&mut self, ctx: &mut Context) -> GameResult {
| ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `ctx`
--> src\main.rs:9:24
|
9 | fn draw(&mut self, ctx: &mut Context) -> GameResult {
| ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
And a window will pop-up.
Hit escape
or click the close button to quit.
If you saw the 2 warnings and the window, you're good to continue!
Use the Loop Luke
Alright! We're ready! Let's use the loop with the context!
For this program, we want to display the duration of each frame in the console along with the text "Hello ggez!"
.
How should we do that? Well, let's look at the 2 callbacks we have in our loop and our State
struct.
There is some information we want to track, so we'll modify State
first.
struct State {
dt: std::time::Duration,
}
dt
is going to represent the time each frame has taken. It stands for "delta time" and is a useful metric for games to handle variable frame rates.
Now in main
, you need to update the State
instantiation to include dt
:
let state = State {
dt: std::time::Duration::new(0, 0),
};
So now that we have state to update, let's update it in our update
callback!
We'll use timer::delta
to get the delta time.
fn update(&mut self, ctx: &mut Context) -> GameResult {
self.dt = timer::delta(ctx);
Ok(())
}
To see the changes in State
, you need to modify the draw
callback.
fn draw(&mut self, ctx: &mut Context) -> GameResult {
println!("Hello ggez! dt = {}ns", self.dt.as_nanos());
Ok(())
}
Every frame, print out Hello ggez! dt = {}ns
. This will print once a frame. Which is going to be a lot.
✔ Check Program
And yet again, run cargo run
.
A window should pop up and your console should be spammed with "Hello ggez!"
and dt's in nanoseconds.
If you see that. Congrats! 🎉
You've successfully bootstrapped a ggez
program.
💪 Challenges
If you are looking to push yourself a bit further on your own, these challenges are for you.
- Display the time in milliseconds or seconds instead of nanoseconds
- Change the size or title of the window that appears
- Print the text to the window instead of the console
- Limit the framerate to 30fps
- Use another callback on
EventHandler
to do something fun