In this part we will shape the basic parts of our game - we will add another tank for an enemy (without a real AI, for now) as well as projectiles, which will have the possibility of impacting and killing our tank or the enemy tank. We will allow ourselves, for now, to have ugly code - the point of this part is how to use nalgebra and ncollide.
Streamlining the Object struct
First thing we should do is streamline the object struct. So far, it has several deficiencies. One such deficiency is that we keep our position and rotation "unwrapped". To fix this, we will use another library, nalgebra:
Extern it in main.rs:
We're going to do the use statement in
We will create a struct to contain our position, rotation and scale
A container for its combination with a sprite, and its implementation including rendering:
Now let's temporarily update our Object struct:
I honestly feel dirty having written that code; the issue is, we have to make
turret public because at the moment we have to set the sprite from our
Our changes in
Object::rot have also fixed turret rotation to be relative to the body, rather than to the world.
Take a breath, we're not done.
Next library we need (unless we want to complicate our code beyond the scope of this tutorial) -
And let's do some
Now, let's start making the actual gameplay. First, we will create an
We're going to rename our old
Object struct to Tank, and do the changes required; the above functions will go into
impl Object for Tank, while the rest will go into
impl Tank. Additionally, we will add collision information to the Tank struct, as well as information for whether it is destroyed or not:
We will also create a new struct called
In the Tank impl, we will also add a function for checking collision with a bullet:
Additionally, we will add a function for firing a bullet in front of our cannon:
Now, let's move to
We will add another player (we will call him player2); for now we won't control this player, and leave him somewhere to the right of player1. Additionally, we will add a vector of bullets:
Of course, also load set the same sprite for player2, and change all uses of player to player1. Additionally, let us position player2 to the right:
We've also created two sprites for the hull and turret, when they are destroyed. We load them and store them for easy cloning. For more info see the finished code for this part. Additionally, we have added a simple sprite for a bullet.
Let us move to the on_update function. We will check to see if any bullet touches a tank, and if so set its is_destroyed bool to true and its sprites to the ones we have created:
We'll also add rendering:
Code available here
Today we will do a couple of of things:
- Make movement sane
- Make our tank be represented by multiple sprites (turret + hull, independently rotated)
- Use mouse movement for it
Movement and Turret
We'll add a current rotation value for our object:
We'll add another function to help us move in the direction we're pointing (or in reverse):
Finally, we make sure the center of the sprite is in the proper place, and represent the rotation:
Now let's add the turret:
We also need to load a separate sprite for the turret and hull. I split
E-100_strip2.png into two images named
I've added a use statement for Pi:
From now on we'll keep the center of our screen:
Now let's catch the mouse movement:
And call the update function:
As one last thing, I've scaled down both the turret and body to about a quarter the size:
You can find the source code for this part here
We're going to jump straight into the action this time around. In our
src folder, we're going to make a new file named
object.rs, containing a new struct - object - in preparation for having multiple game objects:
We include the
object module in
We remove the x and y members of
Game, as they are now redundant and replace it with an
Object named player; we'll also modify our
on_draw functions accordingly:
We've also removed the constant rotation.
Now let's get a sprite for our tank. For that purpose, I found this. In our project folder, we will make a directory named
assets and put a sprite of our preference (I chose
E-100_preview.png) in it.
At this point we need to bring in some new crates;
find_folder to find the assets folder, and a couple of
gfx related libraries (Warning: with version matching the versions required by
piston_window) to allow us to deal with the sprites; I've also updated
piston_window to a more recent version:
We also need to include them:
object.rs we will add the following:
We'll add a sprite attribute to our object struct:
We can now display the sprite, if it is set; first we have to modify the definition of the
render() function slightly:
Now let's display the sprite:
Finally, let's set that up in
main.rs. We'll add an
on_load() function to load our sprite:
And we're done! Building and running our program should look like this:
The source code for this part can be found here.
Last time we had a simple example - our spinning square - but now we want to go one more step and move it with our keyboard. Before we do that though, we should reorganize our code a little.
First, let's create a struct containing the information of our game:
Now let's create an implementation for it. We're going to move our game and rendering logic into an
on_update function and an
on_draw function, with some modifications:
We have also added a function to easily create instances of Game, essentially a constructor.
The event loop should now look like this:
One new thing you will notice is
Event::Render. This is an event that piston uses to tell us it is time to draw a new frame; it also allows for better decoupling of the render loop from the event loop, such that we don't bog down keyboard input with rendering. It also gives us some more values to work with, such as the current window width and height - which we have used in the modified
on_draw code above. We also now capture the
Event::Update(UpdateArgs) member, rather than only the delta time; this is to streamline our event function calls.
The moving the square part
Capturing input is similar to other events in piston:
Before we write
on_input, we need some boolean values so we can do movement based on dt; we'll also add some variables for the x and y of our square:
Of course, in
Game::new() we have to add default values:
Time to write
As you can see, we take the
Input event argument and match it through one of the possible events - two of which, the ones we use, being
Input:Release, representing a button press or release, respectively. We set our
*_d variables accordingly.
The last two things we need to do is add movement logic to the update function:
And change the rendering of the square accordingly:
You can find the source code for this part here
Next time we'll load and display a tank sprite instead of a square. For that purpose, we will also start keeping object information in a separate struct, and its code in a separate file.
Last time, we set up a workspace for rust and cargo. In this part, we are going to go from nothing to a window with a spinning square in the middle.
Creating the project
In a terminal, type the following command:
This creates the folder we are going to work with, as well as an initial configuration for cargo.
Inside the folder, you will find it has created a file,
Cargo.toml and a directory,
src you will find the
main.rs file, with the basic rust "Hello World" program inside it.
Cargo.toml is the configuration file of our project. Open it up - you will find something similar to the following inside:
As you can tell, this section describes the basic infromation of our package - its name, the version it is on and who wrote it (the authors part will be different for you, of course).
Add the following lines at the end of the file:
This tells Cargo that our project will depend on piston_window, a package which serves as a convenience package for a couple of other packages for windowing, input and graphics.
Now open up
src/main.rs and paste in the following code over the old one:
Then build it with
cargo build or (on atom)
Ctrl+Alt+B (WARNING: On Atom I have had issues with build and linter-rust, as the latter also uses cargo and it results in issues as cargo cannot have multiple instances building the same project at the same time). You should get a warning such as
unused variable: 'e' - ignore this for now. Run your program by typing, in a terminal, ./target/debug/piston-tutorial (or double clicking it on windows). You should see something like this:
If the window is "transparent" - don't worry, we will fix that in a second. Otherwise, hurray! We have our first window. Close it by pressing esc.
What's going on?
Most of the code is pretty intuitive - we include the crate
piston_window, bring its contents into the global namespace, create a window with the title "piston-tutorial" and 600 by 600 pixels and tell it to close when we press esc, as well as some boilerplate for displaying the window and retrieving it from our
What might be a little bit confusing is the line
for e in window - it looks like an event loop, but we are iterating over the window?!
The reality is that it's quite simple - by iterating on the window, we get an event (in
e.event) but we also get a copy of the window's state, rather than having issues with
window becoming a moved value. It's also technically correct to write
for e in window.events(), but working with that has some differences.
Now we want to draw our square. To do this, we will call, inside the event loop, a member function of
draw_2d. It takes, as its only argument, a lambda function with two arguments - the graphical context and the graphical instance.
First, let's clear the screen in a nice black:
Now, let's get a transform for the center of the screen and describe the size and colour of the square:
Finally, we can display the square:
Build and run; you should get something like this:
Hurray! One last thing, let's make it spin. We will now use
e.event to get the deltaT and do updates on the update event. Your event loop should now look like this:
And that's it! Running it will have your square spinning around:
You can find the finished source code for this tutorial here.
For the next part, where we will deal with keyboard input, go here
Welcome to the "first" part of this tutorial!
Before jumping into Piston and making a game, we must first have a workspace with the required tools for building rust programs, as well as know enough rust to use piston.
You will need to install the
rust compiler and
cargo, as I will use both to build rust and download libraries I use. On most linux distributions they are generally available in their respective package managers, but on Windows and Mac OS you might have to download the binaries or build the source code; I will not cover this topic in this tutorial
Additionally, you might want a couple of tools to edit your program - I personally use
atom to edit my code, as well as
rust-racer, which integrates with
atom and other IDEs/editors to form quite a complete IDE for rust. Here is a list of atom packages/plugins I use and recommend to work with
language-rust- Language support (highlighting and such) for rust in atom.
linter-rust- Does linting for rust (i.e. highlights errors and warnings in your code)
racer- This package integrates with
rust-racerabove, allowing for code completion in rust
build-cargo- Allows you to build your rust program with cargo with a hotkey
atom-debugger- Use the GDB debugger in Atom
cargo-test-runner- Runs your cargo tests
termrk- A terminal panel inside atom. I find this one very useful, but you might have to fiddle a tiny bit with it to get it working as you intend
Other than these, you might also find the following useful:
build-tools- Allows you to choose from a set of commands you choose, so you can for examples start your program by pressing Ctrl+L and then tapping O. It's a bit wonky, so I don't actually recommend this one
youtube-pane- Use this if you work better while listening to music, but don't install it if you're easily distracted.
https://gist.github.com/colin-kiegel/40432d62c70c2ddf7354 - This is a wrapper for cargo that fixes some odd bugs in the interaction between the
buildpackage when using cargo.
Whew! That was a bit of packages. The bright part is, together they form a solid IDE for rust. Keep in mind it's not necessary to install atom and the packages for atom I have described above, you can use anything that suits you best.
With that done, you can now proceed to the first part of the tutorial.