r/o
1
use std::f32::consts::PI;
2
3
use bevy::{
4
math::ops::{atan2, hypot},
5
prelude::*,
6
};
7
8
const WINDOW_WIDTH: f32 = 1920.;
9
const WINDOW_HEIGHT: f32 = 1200.;
10
11
const UNIVERSAL_FRICTION: f32 = 1.1;
12
13
const PLAYER_STARTING_POSITION: Vec3 = Vec3::new(-600.0, -350.0, 1.0);
14
const PLAYER_STARTING_ANGLE_R: f32 = 0.6;
15
const PLAYER_STARTING_SPEED: f32 = 800.;
16
const PLAYER_STARTING_FIRE_RATE_PS: f32 = 5.;
17
const PLAYER_WIDTH: f32 = 40.;
18
const PLAYER_ACCELERATION: f32 = 800.;
19
20
const BULLET_SIZE: f32 = 5.;
21
const BULLET_SPEED: f32 = 700.;
22
const BULLET_SPREAD: f32 = PI / 16.;
23
24
const SCOREBOARD_FONT_SIZE: f32 = 17.;
25
const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0);
26
27
const BACKGROUND_COLOR: Color = Color::srgb(0.02, 0.018, 0.025);
28
const PLAYER_COLOR: Color = Color::srgb(0.5, 0.35, 0.8);
29
const BULLET_COLOR: Color = Color::srgb(1.0, 1.0, 1.0);
30
const ENEMY_COLOR: Color = Color::srgb(1.0, 0.5, 0.5);
31
const TEXT_COLOR: Color = Color::srgb(0.5, 0.5, 1.0);
32
const SCORE_COLOR: Color = Color::srgb(1.0, 0.5, 0.5);
33
34
const MAX_ROTATION_SPEED_RPS: f32 = PI * 1.5;
35
36
fn main() {
37
let window_plugin = WindowPlugin {
38
primary_window: Some(Window {
39
title: "tähetriiv".to_string(),
40
resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
41
..default()
42
}),
43
..default()
44
};
45
46
App::new()
47
.add_plugins(DefaultPlugins.set(window_plugin))
48
.insert_resource(Score(0))
49
.insert_resource(ClearColor(BACKGROUND_COLOR))
50
.add_event::<CollisionEvent>()
51
.add_systems(Startup, setup)
52
.add_systems(FixedUpdate, (apply_velocity, apply_friction, edges).chain())
53
.add_systems(
54
Update,
55
(
56
handle_player_movement,
57
handle_player_fire,
58
update_scoreboard,
59
),
60
)
61
.run();
62
}
63
64
#[derive(Component)]
65
struct Player;
66
67
#[derive(Component)]
68
struct FireRate(f32);
69
70
#[derive(Component)]
71
struct FireCooldown(Timer);
72
73
#[derive(Component)]
74
struct Bullet;
75
76
#[derive(Component)]
77
struct Enemy;
78
79
#[derive(Component, Deref, DerefMut)]
80
struct Velocity(Vec2);
81
82
#[derive(Component)]
83
struct Friction;
84
85
#[derive(Event, Default)]
86
struct CollisionEvent;
87
88
#[derive(Component, Default)]
89
struct Collider;
90
91
#[derive(Resource, Deref)]
92
struct Score(usize);
93
94
#[derive(Component)]
95
struct ScoreboardUi;
96
97
fn setup(
98
mut commands: Commands,
99
mut meshes: ResMut<Assets<Mesh>>,
100
mut materials: ResMut<Assets<ColorMaterial>>,
101
) {
102
commands.spawn(Camera2d);
103
104
commands.spawn((
105
Player,
106
Mesh2d(meshes.add(Triangle2d::default())),
107
MeshMaterial2d(materials.add(PLAYER_COLOR)),
108
Transform::from_translation(PLAYER_STARTING_POSITION)
109
.with_scale(Vec2::splat(PLAYER_WIDTH).extend(1.))
110
.with_rotation(Quat::from_rotation_z(PLAYER_STARTING_ANGLE_R - PI / 2.)),
111
Velocity(Vec2::from_angle(PLAYER_STARTING_ANGLE_R) * PLAYER_STARTING_SPEED),
112
Friction,
113
FireRate(PLAYER_STARTING_FIRE_RATE_PS),
114
FireCooldown(Timer::from_seconds(0., TimerMode::Once)),
115
Collider,
116
));
117
118
commands.spawn((
119
Text::new("Score: "),
120
TextFont {
121
font_size: SCOREBOARD_FONT_SIZE,
122
..default()
123
},
124
TextColor(TEXT_COLOR),
125
ScoreboardUi,
126
Node {
127
position_type: PositionType::Absolute,
128
top: SCOREBOARD_TEXT_PADDING,
129
left: SCOREBOARD_TEXT_PADDING,
130
..default()
131
},
132
children![(
133
TextSpan::default(),
134
TextFont {
135
font_size: SCOREBOARD_FONT_SIZE,
136
..default()
137
},
138
TextColor(SCORE_COLOR),
139
)],
140
));
141
}
142
143
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
144
for (mut transform, velocity) in &mut query {
145
transform.translation.x += velocity.x * time.delta_secs();
146
transform.translation.y += velocity.y * time.delta_secs();
147
}
148
}
149
150
fn apply_friction(mut query: Query<&mut Velocity, With<Friction>>, time: Res<Time>) {
151
for mut velocity in &mut query {
152
velocity.x -= velocity.x * UNIVERSAL_FRICTION * time.delta_secs();
153
velocity.y -= velocity.y * UNIVERSAL_FRICTION * time.delta_secs();
154
}
155
}
156
157
fn edges(mut commands: Commands, mut query: Query<(Entity, &mut Transform, Option<&Friction>)>) {
158
for (entity, mut transform, maybe_friction) in &mut query {
159
let mut wrapped = false;
160
if transform.translation.x > WINDOW_WIDTH / 2.0 {
161
transform.translation.x -= WINDOW_WIDTH;
162
wrapped = true;
163
}
164
if transform.translation.x < -WINDOW_WIDTH / 2.0 {
165
transform.translation.x += WINDOW_WIDTH;
166
wrapped = true;
167
}
168
if transform.translation.y > WINDOW_HEIGHT / 2.0 {
169
transform.translation.y -= WINDOW_HEIGHT;
170
wrapped = true;
171
}
172
if transform.translation.y < -WINDOW_HEIGHT / 2.0 {
173
transform.translation.y += WINDOW_HEIGHT;
174
wrapped = true;
175
}
176
if wrapped && maybe_friction.is_none() {
177
commands.entity(entity).despawn();
178
}
179
}
180
}
181
182
fn handle_player_movement(
183
gamepads: Query<&Gamepad>,
184
player: Single<(&mut Transform, &mut Velocity), With<Player>>,
185
time: Res<Time>,
186
) {
187
let (mut transform, mut velocity) = player.into_inner();
188
189
for gamepad in gamepads.iter() {
190
let lsx = gamepad.get(GamepadAxis::LeftStickX).unwrap();
191
let lsy = gamepad.get(GamepadAxis::LeftStickY).unwrap();
192
if lsx.abs() > 0.01 || lsy.abs() > 0.01 {
193
let at = atan2(lsy, lsx);
194
let dist = hypot(lsy, lsx).min(1.0);
195
196
let new_rotation = Quat::rotate_towards(
197
&transform.rotation,
198
Quat::from_rotation_z(at - PI / 2.0),
199
time.delta_secs() * MAX_ROTATION_SPEED_RPS * dist,
200
);
201
transform.rotation = new_rotation;
202
}
203
204
let lt2 = gamepad.get(GamepadButton::LeftTrigger2).unwrap();
205
if lt2.abs() > 0.01 {
206
let sa = transform.rotation * Vec3::Y;
207
velocity.x += sa.x * PLAYER_ACCELERATION * lt2 * time.delta_secs();
208
velocity.y += sa.y * PLAYER_ACCELERATION * lt2 * time.delta_secs();
209
}
210
}
211
}
212
213
fn handle_player_fire(
214
mut commands: Commands,
215
mut meshes: ResMut<Assets<Mesh>>,
216
mut materials: ResMut<Assets<ColorMaterial>>,
217
gamepads: Query<&Gamepad>,
218
player: Single<(&mut FireCooldown, &FireRate, &Transform, &Velocity), With<Player>>,
219
time: Res<Time>,
220
) {
221
let (mut fire_cooldown, fire_rate, transform, velocity) = player.into_inner();
222
223
fire_cooldown.0.tick(time.delta());
224
225
for gamepad in gamepads.iter() {
226
let rt2 = gamepad.get(GamepadButton::RightTrigger2).unwrap();
227
if rt2.abs() > 0. {
228
if fire_cooldown.0.finished() {
229
let base_trans = Transform::from_translation(transform.translation.with_z(0.))
230
.with_rotation(transform.rotation)
231
.with_scale(Vec2::splat(BULLET_SIZE).extend(1.));
232
let mut rl = base_trans.clone();
233
rl.rotate_z(BULLET_SPREAD);
234
let mut rr = base_trans.clone();
235
rr.rotate_z(-BULLET_SPREAD);
236
commands.spawn_batch([
237
(
238
Bullet,
239
Mesh2d(meshes.add(Circle::default())),
240
MeshMaterial2d(materials.add(BULLET_COLOR)),
241
base_trans,
242
Velocity(velocity.0 + (base_trans.rotation * Vec3::Y).xy() * BULLET_SPEED),
243
),
244
(
245
Bullet,
246
Mesh2d(meshes.add(Circle::default())),
247
MeshMaterial2d(materials.add(BULLET_COLOR)),
248
rl,
249
Velocity(velocity.0 + (rl.rotation * Vec3::Y).xy() * BULLET_SPEED),
250
),
251
(
252
Bullet,
253
Mesh2d(meshes.add(Circle::default())),
254
MeshMaterial2d(materials.add(BULLET_COLOR)),
255
rr,
256
Velocity(velocity.0 + (rr.rotation * Vec3::Y).xy() * BULLET_SPEED),
257
),
258
]);
259
fire_cooldown.0 = Timer::from_seconds(1. / fire_rate.0, TimerMode::Repeating);
260
}
261
}
262
}
263
}
264
265
fn update_scoreboard(
266
score: Res<Score>,
267
score_root: Single<Entity, (With<ScoreboardUi>, With<Text>)>,
268
mut writer: TextUiWriter,
269
) {
270
*writer.text(*score_root, 1) = score.to_string();
271
}
272