Hands-on Rust 学习之旅(4) ——小鸟飞起来了

在继续上一篇的游戏开发之前,我们来看看成品效果:

fly

主要增加了三个角色:

  1. ‘@’字符充当小鸟,负责上下移动,来躲避迎面而来的障碍物——墙壁;
  2. 就是我们的障碍物,障碍物随着时间从屏幕右侧不断左移;
  3. 还有就是得分,只要小鸟穿过障碍物,即得一分;如果没躲过,则,游戏 Game Over。

Adding the Player

‘@’字符充当小鸟

小鸟的作用在于需要上下移动,正常情况下处于“自由落地”状态,当我们点击空格键,让它飞起来,不至于落下。

跟之前一样,用一个 struct 结构体来表示 Player,主要是坐标位置和上下移动的加速度变化值:

1
2
3
4
5
rust复制代码struct Player {
x: i32,
y: i32,
velocity: f32,
}

首先定一个@字符显示效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rust复制代码impl Player {
fn new(x: i32, y: i32) -> Self {
Player {
x,
y,
velocity: 0.0,
}
}

fn render(&mut self, ctx: &mut BTerm) {
ctx.set(
0,
self.y,
YELLOW,
BLACK,
to_cp437('@')
);
}
}

这个比较好理解,在之后的代码过程中,去重点解释bracket-lib 引擎。

接下来就是定义两个动作,主要是加速度的变化问题,相信看代码能懂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码fn gravity_and_move(&mut self) {
if self.velocity < 2.0 {
self.velocity += 0.2;
}
self.y += self.velocity as i32;
self.x += 1;

if self.y < 0 {
self.y = 0;
}
}

fn flap(&mut self) {
self.velocity = -2.0;
}

Creating Obstacles

创建障碍

障碍物,主要考虑的是缺口的随机出现和缺口得大小。

1
2
3
4
5
rust复制代码struct Obstacle {
x: i32,
gap_y: i32,
size: i32,
}

本文中,缺口的大小和得分有关,随着得分的越多,缺口越小,这也是游戏的难度不断增加,具体看 new 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
rust复制代码impl Obstacle {
fn new(x: i32, score: i32) -> Self {
let mut random = RandomNumberGenerator::new();
Obstacle {
x,
gap_y: random.range(10, 40),
size: i32::max(2, 20 -score)
}
}

fn render(&mut self, ctx: &mut BTerm, player_x: i32) {
let screen_x = self.x - player_x;
let half_size = self.size / 2;

for y in 0..self.gap_y - half_size {
ctx.set(
screen_x,
y,
RED,
BLACK,
to_cp437('/'),
);
}

for y in self.gap_y + half_size..SCREEN_HEIGHT {
ctx.set(
screen_x,
y,
RED,
BLACK,
to_cp437('/'),
);
}
}
}

墙体的设计,主要以缺口大小分成两段来控制 y 坐标,以自己的 x 坐标和小鸟的坐标确定每一祯 x 值,实现不断靠近小鸟的效果。

还需要增加一个小鸟和障碍物碰到的逻辑:

1
2
3
4
5
6
7
8
ini复制代码fn hit_obstacle(&self, player: &Player) -> bool {
let half_size = self.size / 2;
let does_x_match = player.x == self.x;
let player_above_gap = player.y < self.gap_y - half_size;
let player_below_gap = player.y > self.gap_y + half_size;

does_x_match && (player_above_gap || player_below_gap)
}

这代码好理解,就不解释了。

Keeping Score

得分逻辑

得分逻辑就比较简单了,只要小鸟的 x 坐标 > 障碍物的 x 坐标,表示小鸟“飞过”障碍物,并且小鸟没碰上障碍物,则得分+1,并且重置新的障碍物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
python复制代码fn play(&mut self, ctx: &mut BTerm) {
// TODO: Fill in this stub later
ctx.cls_bg(NAVY);
self.frame_time += ctx.frame_time_ms;

if self.frame_time > FRAME_DURATION {
self.frame_time = 0.0;
self.player.gravity_and_move();
}

if let Some(VirtualKeyCode::Space) = ctx.key {
self.player.flap();
}
self.player.render(ctx);
ctx.print(0, 0, "按住空格保持飞翔");
ctx.print(0, 1, &format!("得分:{}", self.score));

self.obstacle.render(ctx, self.player.x);
if self.player.x > self.obstacle.x {
self.score += 1;
self.obstacle = Obstacle::new(self.player.x + SCREEN_WIDTH, self.score);
}

if self.player.y > SCREEN_HEIGHT || self.obstacle.hit_obstacle(&self.player) {
self.mode = GameMode::End;
}
}

其他的参数都是辅助于逻辑的完成,下面我把整个代码贴出来,大家看看也就懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
rust复制代码use bracket_lib::prelude::*;

const SCREEN_WIDTH: i32 = 80;
const SCREEN_HEIGHT: i32 = 50;
const FRAME_DURATION: f32 = 75.0;
struct Player {
x: i32,
y: i32,
velocity: f32,
}

impl Player {
fn new(x: i32, y: i32) -> Self {
Player {
x,
y,
velocity: 0.0,
}
}

fn render(&mut self, ctx: &mut BTerm) {
ctx.set(
0,
self.y,
YELLOW,
BLACK,
to_cp437('@')
);
}

fn gravity_and_move(&mut self) {
if self.velocity < 2.0 {
self.velocity += 0.2;
}
self.y += self.velocity as i32;
self.x += 1;

if self.y < 0 {
self.y = 0;
}
}

fn flap(&mut self) {
self.velocity = -2.0;
}
}

struct Obstacle {
x: i32,
gap_y: i32,
size: i32,
}

impl Obstacle {
fn new(x: i32, score: i32) -> Self {
let mut random = RandomNumberGenerator::new();
Obstacle {
x,
gap_y: random.range(10, 40),
size: i32::max(2, 20 -score)
}
}

fn render(&mut self, ctx: &mut BTerm, player_x: i32) {
let screen_x = self.x - player_x;
let half_size = self.size / 2;

for y in 0..self.gap_y - half_size {
ctx.set(
screen_x,
y,
RED,
BLACK,
to_cp437('/'),
);
}

for y in self.gap_y + half_size..SCREEN_HEIGHT {
ctx.set(
screen_x,
y,
RED,
BLACK,
to_cp437('/'),
);
}
}

fn hit_obstacle(&self, player: &Player) -> bool {
let half_size = self.size / 2;
let does_x_match = player.x == self.x;
let player_above_gap = player.y < self.gap_y - half_size;
let player_below_gap = player.y > self.gap_y + half_size;

does_x_match && (player_above_gap || player_below_gap)
}
}

enum GameMode {
Menu,
Playing,
End,
}

struct State {
player: Player,
frame_time: f32,
obstacle: Obstacle,
mode: GameMode,
score: i32,
}

impl State {
fn new() -> Self {
State {
player: Player::new(5, 25),
frame_time: 0.0,
obstacle: Obstacle::new(SCREEN_WIDTH, 0),
mode: GameMode::Menu,
score: 0,
}
}

fn restart(&mut self) {
self.player = Player::new(5, 25);
self.frame_time = 0.0;
self.mode = GameMode::Playing;
}

fn main_menu(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "Welcome to Flappy Dragon");
ctx.print_centered(8, "(P) Play Game");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
self.player = Player::new(5, 25);
self.frame_time = 0.0;
self.obstacle = Obstacle::new(SCREEN_WIDTH, 0);
self.mode = GameMode::Playing;
self.score = 0;
}

fn dead(&mut self, ctx: &mut BTerm) {
ctx.cls();
ctx.print_centered(5, "You are dead!");
ctx.print_centered(6, &format!("You earned {} points", self.score));
ctx.print_centered(8, "(P) Play Again");
ctx.print_centered(9, "(Q) Quit Game");

if let Some(key) = ctx.key {
match key {
VirtualKeyCode::P => self.restart(),
VirtualKeyCode::Q => ctx.quitting = true,
_ => {}
}
}
}

fn play(&mut self, ctx: &mut BTerm) {
// TODO: Fill in this stub later
ctx.cls_bg(NAVY);
self.frame_time += ctx.frame_time_ms;

if self.frame_time > FRAME_DURATION {
self.frame_time = 0.0;
self.player.gravity_and_move();
}

if let Some(VirtualKeyCode::Space) = ctx.key {
self.player.flap();
}
self.player.render(ctx);
ctx.print(0, 0, "按住空格保持飞翔");
ctx.print(0, 1, &format!("得分:{}", self.score));

self.obstacle.render(ctx, self.player.x);
if self.player.x > self.obstacle.x {
self.score += 1;
self.obstacle = Obstacle::new(self.player.x + SCREEN_WIDTH, self.score);
}

if self.player.y > SCREEN_HEIGHT || self.obstacle.hit_obstacle(&self.player) {
self.mode = GameMode::End;
}
}
}

impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
// ctx.cls();
// ctx.print(1, 1, "Hello, Bracket Terminal!");
match self.mode {
GameMode::Menu => self.main_menu(ctx),
GameMode::End => self.dead(ctx),
GameMode::Playing => self.play(ctx),
}
}
}

fn main() ->BError {
println!("Hello, world!");

let context = BTermBuilder::simple80x50()
.with_title("Flappy Dragon")
.build()?;

main_loop(context, State::new())
}

整个运行效果,就是开篇的那样:

fly

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%