I find nothing more frustrating in car games than when the developer has implemented some ridiculously unrealistic model for how the car moves. For most games, especially flash-based mini-games, simulation grade 3D physics are not required, but still the car should move and turn roughly like you'd expect a real car to.

Have a quick go at this car parking game, or this one, try to turn into a parking space, something doesn't feel right. More specifically the rear wheels are sliding sideways whenever you turn - this does not (normally) happen in a real car, and it makes this game very hard for all of us used to controlling a real car. I'll show you here how to get better physics than this in just half a dozen lines of code.

The very simple assumption that we will start with is that each wheel can only move in the direction it is pointing. This is actually a very good approximation for normal driving, and exactly appropriate for these kinds of parking games. The other simplification we will make is to use what is called a bicycle model, we imagine that the car has just two wheels; one at the front in the middle to steer, and one at the back in the middle that cannot steer. Of course we can draw a real car with four wheels, but the physics will only be considering two wheels at the centre of each axle.

To keep track of the state of our car in the game we probably have something like:

Vector2 carLocation; float carHeading; float carSpeed; float steerAngle; float wheelBase; // the distance between the two axles

Our physics algorithm should update the carLocation and carHeading each frame according to the above assumptions, and the input part of our game can update carSpeed and steerAngle based on the user input. So how to actually calculate the next position and heading of the car? It can be broken down into three steps:

- Find the world position of the front and back imaginary wheels

- Move each wheel forward in the direction it is pointing

- Calculate the new car location and heading from the new wheel positions

**Step 1**

We can use geometry calculations to find the actual positions of the wheels (which are at the centre of each axle). The gap between the axles is given by the wheelBase, so each wheel is half that distance from the car centre, in the direction that the car is facing. The following diagram shows the geometry:

And the code:

Vector2 frontWheel = carLocation + wheelBase/2 * new Vector2( cos(carHeading) , sin(carHeading) ); Vector2 backWheel = carLocation - wheelBase/2 * new Vector2( cos(carHeading) , sin(carHeading) );

**Step 2**

Each wheel should move forward by a certain amount in the direction it is pointing. The distance it needs to move depends on the car speed, and the time between frames (I'll call it dt here). The rear wheel is easy, it moves in the same direction the car is heading. For the front wheel, we have to add the steer angle to the car heading as the diagram shows:

And the code:

backWheel += carSpeed * dt * new Vector2(cos(carHeading) , sin(carHeading)); frontWheel += carSpeed * dt * new Vector2(cos(carHeading+steerAngle) , sin(carHeading+steerAngle));

**Step 3**

The new car position can be calculated by averaging the two new wheel positions. The new car heading can be found by calculating the angle of the line between the two new wheel positions:

In code:

carLocation = (frontWheel + backWheel) / 2; carHeading = atan2( frontWheel.Y - backWheel.Y , frontWheel.X - backWheel.X );

And that's it - a total of 6 lines of code to get a simple car to move realistically.

Hello,

ReplyDeleteThanks for your code. It was helpful, but i'm still having problems to make the car move right.

My car looks like in a soap road...sliding or spinning like a fan.

Can you give some example using the structure of a XNA Game? Like...putting the code into the right methods?

Best regards.

you have to add friction you dumb dumb

DeleteAny Idea how to implement this

DeleteAssuming you have a Car class, you would need to put the 5 fields I mention at the top in this Car class. The Car's Update method would then include the 6 lines I mentioned. You probably also want to set the carSpeed and steerAngle variables based on some keyboard input.

ReplyDeleteIn the Car's Draw method you would then use carLocation and carHeading to actually draw the car.

Hello engineer, I am doing a project on opengl to drive a tank, the tank was given to us the issue is to move the tank, could you explain your code a bit more. I am not also aable to add carLocation with wheelBase for some reason. any pointers would help

DeleteVector2 is the XNA class for a 2D vector. You cannot add a scalar (like wheelBase) to a 2D vector. The first line of code multiplies wheelBase by a 2D direction vector (which results in a new 2D vector scaled by the value of wheelBase) and then adds that to carLocation.

DeleteSweet.. Breaking down into two steps is something i had never considered.. thnx :)

ReplyDeleteHey, I think there's a problem with this algorithm. It doesn't conserve the baseline, that is, the distance between the front and rear tire. Over time, if you move forward while turning, the baseline will shrink down to carSpeed * dt, and the motion will become completely unrealistic. I tested it out with an initial baseline of 50, carSpeed = 50, dt = 1/60, and steerAngle = 20 degrees. By frame 500 the baseline had shrunk down to 26.2.

ReplyDeleteMaybe the most realistic way to rectify this is to have the front tire move extra in order to maintain the baseline. I get that the front tire must move a distance of sqrt(B^2+C)-B, where:

B = (b-r) cos(steerAngle)

C = r(2b-r)

b is the size of the baseline

r = carSpeed * dt is the distance the rear wheel moves this frame

If you use this quantity in place of r in your update of frontWheel, the baseline should be conserved.

I noticed this too. The change you added seemed to work.

DeleteThanks for the reply Christopher, however you need to re-calculate all 6 lines every frame. This will recalculate the front and rear wheel positions from scratch based on the car center point and heading every frame. You'll then find the distance between the two wheels is always equal to wheelBase.

ReplyDeleteAh, I understand. Yes, that definitely means the problem I was seeing was my mistake. It looks good!

ReplyDeleteI think that my modification makes it slightly more realistic, but it's probably not worth the effort. Thanks for the response!

Yes your solution is definitely more accurate, and at very sharp steering angles (nearer to 90 degrees rather than straight ahead) it should make a visible difference. I think we also have to modify the rear wheel distance, as at a 90 degree steer angle the rear wheel should stay stationary - also how to define what carSpeed means in this situation?

ReplyDeleteBut whichever way you use, it's not a good idea to keep track of each wheel totally separately, they will drift apart due to numerical precision errors no matter how good the maths is.

how you work with floats and int conversion?

ReplyDeletecos(angle)* speed; 2.40232;

posx and posy are natural numbers 1,2,3,4 ...

i have troubles with that.. the "car" is moving to the left and up the screen.

(i use SDL to simulation)

Here I'm just using floats, but I understand the reasons for wanting to avoid them for posx and posy (accuracy loss with large values).

ReplyDeleteIf cos(angle)*speed*dt is coming out around 2.4, then you shouldn't use ints alone for the position, there isn't enough resolution to make the car move the direction it is facing.

If you need to avoid doubles/floats then try fixed point, it will work if you use enough bits in the fractional part, but the math become a little more complex. You might even need to use a separate variable to store the fractional part of the result (and then posx and posy can remain as integers). This should solve your problem.

hello. i hope this isn't too much to ask, but could you do the same example, but without the vectors. this is like the only example ive found on the net i can understand, all the others are waay to complicated. the problem im having is that i write in blitz basic. and it doesn't use vectors. ive tried converting your code but it doesnt seem to work for me. so could you try to write the code again, except without all the vector operations?

ReplyDeleteHi Anonymous, it will work if you just do the x and y component separately for each line.

ReplyDeleteIn your case you will need to keep track of carLocationX and carLocationY as separate variables from frame to frame. Within each frame you then need to do the steps I mentioned, but split into X and Y components. Something like this:

frontWheelX = carLocationX + wheelBase/2 * cos(carHeading);

frontWheelY = carLocationY + wheelBase/2 * sin(carHeading);

backWheelX = carLocationX - wheelBase/2 * cos(carHeading);

backWheelY = carLocationY - wheelBase/2 * sin(carHeading);

backWheelX += carSpeed * dt * cos(carHeading);

backWheelY += carSpeed * dt * sin(carHeading);

frontWheelX += carSpeed * dt * cos(carHeading+steerAngle);

frontWheelY += carSpeed * dt * sin(carHeading+steerAngle);

carLocationX = (frontWheelX + backWheelX) / 2;

carLocationY = (frontWheelY + backWheelY) / 2;

carHeading = atan2( frontWheelY - backWheelY , frontWheelX - backWheelX );

Let me know if you're still having problems.

thank you so much it works perfectly now. i see what i did wrong, i had the variables aligned in the wrong order. lol now have to learn how to make the car drift. *prepares self for difficult journey into car physics*

ReplyDeleteIf you want physically correct drifting with realistic control, then you want to totally ignore this tutorial and start again considering forces and accelerations on the tyres. However there is a quick hack I found that you can add to this tutorial that gives a rough impression of drifting for very little effort.

ReplyDeleteThe trick is to create a delay on the carHeading value, so that the value used in the sin and cos terms when moving the wheels forwards, is the value from a few frames earlier. But still use the instantaneous value for drawing the car, so this gives the impression that the car is sliding sideways a bit (the length of the delay controls how much the car will slide). It might take some tweaking to get it to work, because after all this is just a pure hack to give a very rough approximation of drifting.

Hi

ReplyDeleteFirstly thanks for the great tutorial. I'm having a bit of a problem though (using C#). My car heading value keeps getting stuck between two values and my car just rotates a little to the left and then back to the right, even when I am holding just one key (eg. 'a' key).

I'm using this atm

carHeading = (float)Math.Atan2(Convert.ToDouble(frontWheel.Y - backWheel.Y), Convert.ToDouble(frontWheel.X - backWheel.X));

I have to convert to double because the functions take doubles, but then i have to change back to float because Vectors use float.

Thanks if you can help me any.

Hi James, that line looks fine to me, the mistake must be in the rest of your code. Are you running all 6 lines of code every frame? What if you fix steerAngle and carSpeed to be constant - does the car drive around in a circle?

ReplyDeleteHey TheEngineer

ReplyDeleteI think i may have fixed it! And it is all thanks to you suggesting fixing the steerAngle and carSpeed to a constant. That really helped me find out what was really going on. Good call :)

It seems to be working as expected right now.

What i'd like to try now is to get the steer angle to sort of square up with the car heading while not holding down the left or right (a or d) keys. If you know what i mean? At the moment. Sort of how when you let go of the steering wheel in a real car the wheels will go back straight. I think this might just be as easy as adding or subtracting to the steering angle until it is the same as the car heading?

Once again thanks!

- James

Hi James, glad you got it working. To return the steering angle back to zero, I usually just subtract a fixed amount back towards zero when neither left nor right are being pressed. Watch out not to overshoot zero though. Something like:

ReplyDeleteconst float returnRate = 0.01 * dt;

if( !A_Pressed && !D_Pressed)

{

float delta = steerAngle;

if( delta > returnRate ) delta = returnRate;

if( delta <-returnRate ) delta =-returnRate;

steerAngle -= delta;

}

You can adjust returnRate to set how quickly the wheels return back to centre. I prefer it to be a bit faster than the rate at which the wheels turn when holding down the key - you then let the player get some quite smooth turning by just tapping the a or d keys.

You could even make returnRate proportional to the car speed, that might be more realistic (when you are stopped the wheels then wouldn't return back, which is correct).

Thanks for the simple and easy to follow tut on steering.

ReplyDeleteFor those interested in drifting, another option/hack is to create a 'carDriftHeading' variable that is calculated something like this;

carDriftHeading = carHeading + (steerAngle * (carSpeed/carSpeedMax) * driftDampening)

This doesn't actually produce any real drift, rather the illusion of drift by over rotating the car at high speeds so may not suit some situations but it will give almost no drift at low speeds and a lot of drift at high speeds so it is somewhat realistic and when tunned right, provides a fun driving experience.

I suspect combining a little of this approach and a little of the approach

'The Engineer' mentions above might be enough to get a lo-fi game up and running.

Hey TheEngineer,

ReplyDeleteThanks for the tutorial. I have been looking for days for a simple car driving algorithm for days!

That said, I'm having some problem with my code. For some reason my car simply jitters in place, maybe moving like 2 pixels back and forth not sure why. I think the problem might be converting to and from Radians to Degrees. The SteerAngle is set to 45 degrees and the CarSpeed is 1.5f.

float controlAngle = 45f;

float carFacingAngle = MathUtils.radiansToDegrees * mBody.getAngle();

steerAngle = controlAngle - carFacingAngle; // angle from Car to Control

carSpeed = 1.5f;

frontWheel.x = carLocation.x + wheelBase/2 * MathUtils.sin(carHeading);

frontWheel.y = carLocation.y + wheelBase/2 * MathUtils.cos(carHeading);

backWheel.x = carLocation.x - wheelBase/2 * MathUtils.sin(carHeading);

backWheel.y = carLocation.y - wheelBase/2 * MathUtils.cos(carHeading);

backWheel.x += carSpeed * deltaTime * MathUtils.sin(carHeading);

backWheel.y += carSpeed * deltaTime * MathUtils.cos(carHeading);

frontWheel.x += carSpeed * deltaTime * MathUtils.sin(carHeading+steerAngle);

frontWheel.y += carSpeed * deltaTime * MathUtils.cos(carHeading+steerAngle);

carLocation.x = (frontWheel.x + backWheel.x) / 2;

carLocation.y = (frontWheel.y + backWheel.y) / 2;

carHeading = MathUtils.atan2( frontWheel.y - backWheel.y , frontWheel.x - backWheel.x );

I swapped the Sin and Cos because in LibGDX angles are backwards I think, so 90 degrees faces left instead of up.

Hi Abdullah,

DeleteI'm not sure how you are calculating steerAngle, but this is meant to be the angle of the front wheels relative to straight ahead. Try setting this to 0 and check the car goes straight. If not I would simplify the whole thing down to just one line like:

carLocation.x += carSpeed * deltaTime;

Then at least the car should move horizontally at a constant speed (although it might not face the right way). If even that doesn't work then you've got an error somewhere else in your code.

Let me know how it goes.

So I found out what the problem is. Apparently in LibGDX Sprites and Box2d Bodies have their positions at the bottom left corner of the object. So the carLocation would be (0,0) instead of say (3,3) for an object that has all sides the length of 6.

DeleteI guess what I need to do is figure out a way to either move the origin of the position to the middle of the sprite or reset the calculations to take that into account.

I think I'll try to fix the positioning so that the return position is the center not the bottom left.

Thanks for this article, i was loocking for a simple steering physics and it helps me a lot.

ReplyDeleteAs the rear wheel should follow the front wheel, may i propose some modifications to make it slightly more realistic :

// Old front wheel position

Vector2 frontWheel = carLocation + wheelBase/2 * new Vector2( cos(carHeading) , sin(carHeading) );

// New front wheel position

frontWheel += carSpeed * dt * new Vector2(cos(carHeading+steerAngle) , sin(carHeading+steerAngle));

// Old back wheel position

Vector2 backWheel = carLocation - wheelBase/2 * new Vector2( cos(carHeading) , sin(carHeading) );

// new car heading

carHeading = atan2( frontWheel.Y - backWheel.Y , frontWheel.X - backWheel.X );

// new car location

carLocation = frontWheel - wheelBase/2 * new Vector2( cos(carHeading) , sin(carHeading) );

Thanks for your alternative algorithm idea - it got me thinking! It's easy to test the accuracy of your idea by using a specific input to the model with a very small timestep and then seeing how inaccurate it gets with larger timesteps.

DeleteSo you can hopefully replicate the results I used the following settings: Car starting with centre point at (0,0) and heading 0 degrees, wheel base of 2 units, a speed of 1 unit per second and a steer angle of 5 degrees. I ran this setup for 1 second simulation time and then output the final car heading.

First off I used a dt of 0.001 and got 2.497 degrees with my algorithm described above and 2.496 degrees with your algorithm - so it seems pretty close (I assume using a smaller dt will get even closer results). Then I adjusted the timestep to 0.01 and got 2.497 again with my algorithm, but yours gave 2.484. With a dt of 0.1 mine still gives 2.497 and yours is down to 2.378.

I think the source of the error in your algorithm is that you are trying to calculate the new angle of the car based on the old rear wheel position and the new front wheel position, so the result will obviously depend heavily on the value of the time-step (which it shouldn't). Mine uses the new updated position of both wheels so does not depend as much on the time-step value.

The thing that initially rang alarm bells with your idea was that there is no physical reason why the front wheel should be treated any differently to the rear wheel - both should be moved forwards the direction they are pointing each time step, and then it doesn't matter if the car is going forwards or backwards, or if the front or rear wheels (or both) are being steered.

Thank you so much for this! :o)

ReplyDeleteCould I just ask what the draw code would be, as I am getting it all wrong, with regards to position and rotation of the sprite itself.

Hi AWP, I'm using XNA in 3D for this, so my draw code simply builds a world matrix from the position and rotation of the car:

DeleteMatrix mWorld = Matrix.CreateRotationY( heading ) * Matrix.CreateTranslation( location.X , -axleHeight + axles[0].wheelRadius, location.Z );

This is then set in the normal way before drawing the mesh.

In 2D you need to set the rotation of the sprite to "heading" and then translate it according to "location".

Thanks for the reply! Will try what you suggested.

ReplyDeleteHello man!

ReplyDeleteFirst of all, good tutorial! You have solved many questions for me with this.

Now, my problem: i'm trying to adapt these code to Python, using Pygame.

But, this isn't working good: the parts of my car are going in strange directions when I try to change the angle. But works perfectly using direction = PI, angle = 0.

Can you help me with this?

The code:

distance = v*dt

frontX = centerX + (math.cos(angle)*base/2) + (math.cos(angle+direction)*distance)

frontY = centerY + (math.sin(angle)*base/2) + (math.sin(angle+direction)*distance)

backX = centerX - (math.cos(angle)*base/2) + (math.cos(angle)*distance)

backY = centerY - (math.sin(angle)*base/2) + (math.sin(angle)*distance)

meioX = (frontX + backX)/2

meioY = (frontY + backY)/2

direction = math.atan((frontY- backY)/(frontX - backX))

I forgot to wish you happy holidays new year! Happy new year!

DeleteCarlos, your last line of code "direction=..." should be "angle=...". Using more descriptive variable names helps to avoid mistakes like this.

DeleteMerry Christmas!

Hi, nice code up in there, but I think there should be some correction in rear wheel movement, don't you think? I agree that each wheel can only move in its pointing direction and that makes the front wheels to move correclty. But when you are making a turn, the very next car position has always a new carHeading. That makes the rear wheels to have a new direction as well.

ReplyDeleteWith that in mind, your algorithm can only work with small dt in time increment. Or rather, your accuracy will be better the smaller your dt is.

Think about a very tight turn with a large time step. The front wheels will move only a few to the front direction and a lot to the side direction.With this large time step, the next position of the rear wheels will not only be in the old carHeading direction but also will drift a little bit to the side of the turn.

I'm not very good in expressing myselft, but can you understand what I am saying? If so, can you think of a good solution for this?

Take a look at this image I've just made:

Deletehttp://i155.photobucket.com/albums/s312/Felipe_Rbias/car_physics_zps47f1e89a.png

it represents what I'm trying to say. This still may not be the perfect solution but may be a good approximation making both green angles to be equal.

Hi Felipe, yes I understand your concern about the errors with large values of dt. I wrote the code with a frame rate of 30 or 60 fps in mind, and with this value of dt and "normal" car turning speeds the error is very small (less than 1%). The idea was to show that with only a few lines of code you can get a much better handling car than many web-games out there.

DeleteIf you did want to do it 100% accurately (eg you have a much bigger value of dt) it would be quite straightforward (but more than 6 lines of code!). First you need to find the instantaneous centre of rotation of the car, do this by drawing two lines out perpendicular from each wheel and find where they cross. Each wheel will then follow an arc around this centre point. Then for your value of dt you can calculate how far along the arc the wheel will have moved, and use this as the new positions. You value of dt can be as large as you want then and it will still be accurate, so long as the steering angle is constant throughout the time period.

Yes, that is it! And I agree that for small dt your code is very good indeed. This arc method is also very interesting and quite simple. I did not think about that. Thanks anyway and good luck with your blog!

DeleteThis comment has been removed by a blog administrator.

ReplyDeleteSteering goes bananas when the car reverses, has anyone else found this, or have I got something wrong?

ReplyDeleteI fixed the steering problem I had, but I can't remember exactly what I did, it wasn't anything to do with your code.

ReplyDeleteGlad you got it working :-)

DeleteThe blog is unique that’s providing the nice material. Please post more interesting articles here.

ReplyDeleteclick Here

عندما يتعلق الأمر بالحفاظ على منزل أنيق ومرتب، لا تجعل تجتاح ScrewYOU عليك التخلي طويلة قبل ان الوقت قد حان لفصل الربيع التنظيف. بدلا من ذلك، كسر الوادي اللعين وظيفة (غسل الملابس، تنقية الحوض) بانخفاض في الاشياء الصغيرة يمكنك المسيل للدموع هنا وهناك. وهذه التغييرات الصغيرة إلى روتينك تساعد على الحفاظ على نظافة المنزل ومنظم مع الحد الأدنى من الوقت والجهد.

ReplyDeleteشركة نقل اثاث بخميس مشيط

شركة تنظيف فلل بخميس مشيط

شركة مكافحة حشرات بخميس مشيط

شركة كشف تسربات المياه بخميس مشيط

شركة تنظيف بخميس مشيط

شركة تنظيف بالرياض

شركة نظافة بالرياض

شركة تنظيف منازل بالرياض

شركة تنظيف فلل بالرياض

شركة كشف تسربات المياه بالرياض

شركة نقل اثاث بالرياض

شركة تخزين اثاث بالرياض

شركة مكافحة حشرات بالرياض

شركة مكافحة النمل الابيض بالرياض

شركة رش مبيدات بالرياض

ReplyDeleteشركة تسليك مجارى بالرياض

شركة عزل اسطح بالرياض

شركة تنظيف خزانات بالرياض

شركة عزل خزانات بالرياض

شركة تنظيف موكيت بالرياض

شركات تنظيف بخميس مشيط

كورة اليوم

شركة تنظيف بالرياض

كورة تويتر

كورة جوجل بلس

قصر جوجل بلس

مكافحة حشرات بخميس مشيط

شركات تنظيف بخميس مشيط

كشف تسربات المياه بخميس مشيط

كورة اليوم

Detectionofwaterleaksksa

شركة نقل اثاث بالجوف

اخبار كورة

Thanks for sharing this excellent data. Thank you!

ReplyDeleteCan you please help with a force diagram for a basic car steering? Anyone ? Please?

ReplyDeleteThis is exactly what I was looking for. Thanks a lot!

ReplyDeleteGreat tutorial - I used Carlos Jr's implementation as the engine I'm using doesn't have vectors. :)

ReplyDeleteHas anyonѥ tried implementing a full Ackermann in 2d?

https://en.wikipedia.org/wiki/Ackermann_steering_geometry

Using Unity 2D, C# and physics, how does one take the rotation of the main body of a top down car and add the maximum steering angle to tell the wheels which way to point. It seems that .rotation doesn't work past 360 degrees and .Rotate just spins the wheel.

ReplyDeleteThank you

This comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteThis comment has been removed by the author.

ReplyDeleteYou need to know trigonometry in this car game. I never liked it when I was in high school.

ReplyDeleteThis content is written very well. Your use of formatting when making your points makes your observations very clear and easy to understand. Thank you. Best PS4 Racing Wheel 2019:Expert's Advice

ReplyDeleteThis content is written very well. Your use of formatting when making your points makes your observations very clear and easy to understand. Thank you. Best 2 In 1 Computer:Reviews,Guide,Top Rated

ReplyDeleteThank you for the excellent article. Exactly what I was looking for. I think the simplest solution to the 'shriking car' problem of front and rear wheels getting closer together after a turn is the following:

ReplyDelete1) Use the algorithm as written to find the new position of the front and rear wheels.

2) Make a line between the original position of the rear wheels and their new position.

3) Find the point on this line where the distance to the front wheels is correct. This will be your rear wheel position.

Using this technique, the front wheels would be travelling at the current speed and the rear wheels slightly less depending on turn angle.

When I was told about Pass4sure Cisco dumps, first I downloaded free demo questions from Dumpspass4sure that gave me an opportunity to look into the material. All the information was authentic and the pattern of Pass4sure Cisco questions and answer was fully valid. I thank for this guide and appreciate such services.

ReplyDeleteMany Autos is private limited company manufacturing Autos sapre parts in reading.Many Autos best spare abs sensor, abs pump,abs ring,ABS Hydraulic Units etc. Many autos also proving door to door repairing services.

ReplyDeleteThanks for sharing this valuable information regarding

ReplyDeletewill help you more:

you want to make a game using Canvas and HTML5? Follow along with this tutorial and you'll be on your way in no time.

HTML Game Tutorial