This tutorial/article is going to be somewhat underdeveloped and possibly even wrong sometimes, but I've been asked to explain this twice and I know a lot of people have trouble with it. Please leave questions and clarifications in the comments and they may be incorporated into the copy.
To get the most use out of this tutorial, you will need to attempt to understand the math concepts and shapes behind it, NOT just the specific code. By copying without understanding, you do not learn.
It should help answer questions like:
(Movement Section)
How do I get a sprite to follow the player?How do I move a bullet at an angle?
(Smoothing Section)
What can I do to improve the aesthetic quality of my game?How can I smoothly interpolate between two values?Why radians?
(General)
What is SIN() useful for?How does this crap even work? My teacher didn't teach wellWhy everything gotta cost radians?
You're probably here because, like I got in middle school, your education (or future education on) trigonometry consisted of definitions and memorization, and you know there's SOMETHING about angles that you need to work with in your game but you don't really get it.
Trigonometry is a scary word that means "math about angles and triangles."
All of the useful stuff we can do with it regarding movement is based on this concept of the "unit circle," a circle with a radius of 1 unit centered around the origin (the point (0,0) on a coordinate system). Angles are counted counter-clockwise.
"All the way left" is (-1,0) on this circle (180 degrees)
"All the way right" is (1,0) (0° or 360°), and similarly for up and down, (0,1) (90°) and (0,-1) (270°), respectively.
But what about points in non-cardinal directions? Like 45 degrees?
Well, let's consider the "radius" of the circle as the hypotenuse of a triangle.
We actually know the length of the hypotenuse, since the circle has a radius of 1. But what's so useful about that? Actually, the interesting part is the other two sides. You'll notice they're perpendicular and axis-aligned, which doesn't seem important, but consider that it doesn't form a bad triangle like this one:
:(
Anyway, the length of sides can be calculated with sine and cosine.
If you remember, the sine of an angle is the ratio between the opposite side and the hypotenuse, and the cosine is the ratio between the adjacent side and the hypotenuse.
Since the hypotenuse is always 1 on the unit circle, we're effectively calculating b and h!
So the sine of angle θ (theta) is b and the cosine of angle θ is h. In SmileBASIC, that looks like this:
VAR ANG = RAD(90)
'SIN() and COS() take "radians,"
'we have to transform our angle in degrees
'to radians with RAD()
?SIN(ANG) ' 1
?COS(ANG) ' 0
Remember that 90° is perfectly north on the circle, a Y-offset of 1 and an X-offset of 0.
That's right. SIN() gives us the Y component of an angle, and COS() gives us the X component. Now it's starting to make sense why we'd want this, right?
"But Miss Yuuka," you say, "how do we find the angle?"
Introducing: arctangent!
Arctan, or ATAN actually has two modes in SmileBASIC, but the one we like is the second form
ATAN (2): Returns the arc tangent value (from XY-coordinates)
Variable = ATAN( Y-coordinate,X-coordinate )
Given two coordinates relative to the origin (0,0), ATAN gives us the angle of the slope (which you might remember as being y/x) of that point from the origin. Let's apply that to a VERY real scenario that I'm sure you have all encountered.
the angery [sic] blob wants to attack the happy blob, but it doesn't know how to move. In this case, we want to consider the angery blob's position as the origin, and the happy blob a target point.
If we want to find that angle, we'll have to transform the happy blob's coordinates (which are now in the SB screen coordinate system) into a new coordinate system centered around the ANGERY blob. And creating a new coordinate system takes a lot of work!
No. Not really.
C_X = TARGET_X - ENEMY_X
C_Y = TARGET_Y - ENEMY_Y
All we do is find the difference between the points. In fact, we're calculating the lengths of the legs of the triangle right now, so why bother with ATAN or any of that? Because if we used these distances, we'd jump right to the other blob! We have to normalize them to a travel distance of exactly 1... just like that unit circle we had earlier.
So given these distances, we can plug them into ATAN to get the angle (in radians), and then from there, use SIN and COS to get the horizontal and vertical components of our movement.
'remember, Y comes first
ANG = ATAN(C_Y, C_X)
STEP = 1 ' distance to move, "speed" if you want
ENEMY_X = ENEMY_X + COS(ANG) * STEP
ENEMY_Y = ENEMY_Y + SIN(ANG) * STEP
If we had this in a real loop with SPOFS, the "enemy" would chase the player around.
Here's a full example where a sprite shoots something at you (without collision or anything)
SPSET 0,84
SPSET 1,5
VAR X, Y, DX, DY, EX=200, EY=120, BX, BY, ANG
SPOFS 1, EX, EY
WHILE 1
'player movement
STICK OUT DX,DY
INC X, DX*2
DEC Y, DY*2
SPOFS 0,X,Y
'the relevant part
IF !SPUSED(2) THEN 'keep shooting, see L27
SPSET 2, 257
SPOFS 2, EX, EY
ANG = ATAN(Y - EY, X - EX) 'get angle from difference in position
'but calculate it once; this is a bullet
ENDIF
SPOFS 2 OUT BX, BY 'get current coordinates
'get horizontal part with COS
BX = BX + 5 * COS(ANG) ' *5 for SPEEEED
'get vertical part with SIN
BY = BY + 5 * SIN(ANG)
SPOFS 2, BX, BY
IF SQR(POW(BX,2)+POW(BY,2)) > 512 THEN SPCLR 2 'destroy it after it goes far enough
VSYNC
WEND
That's about it.
In Section 2 we exploited one characteristic of the unit circle, its unit radius, to move a sprite based on a given angle.
Here we'll exploit another property: it's a circle!
If we were to move, say, 10 degrees from 0, what would the change in vertical position (or Δsin(θ)) be?
hmm. Though it doesn't matter what in particular the value is, let's calculate it in SmileBASIC:
?SIN(RAD(10)) - SIN(RAD(0))
0.17364818
OK
But since it's a circle, the arc (a section of a circle) at the "top" should have less vertical change in the same 10 degrees.
Let's test (recall that the top of the circle is 90 degrees):
?SIN(RAD(90)) - SIN(RAD(80))
0.01519225
OK
That's almost a tenth less vertical change than the first 10 degrees!
Okay now check out this graph of of sine:
This is just a graph of the values of sin(x) for x = 0° to x = 360°. Kinda makes sense, doesn't it? First it goes up, then comes back down, down into negative values after 180°, and back to 0.
Sine is a periodic function: that is, it repeats after a certain interval. Specifically, the period of sine is 2π radians.
One unit for measuring angles that we've been using is degrees. Another common unit is radians. Actually, we've already been using them, by converting degrees with RAD(), but I haven't actually discussed this yet. One radian is equal to the length of the radius of the circle, measured along the perimeter.
Lucas Barbosa provided this excellent animation to visualize the radian over on Wikipedia:
You'll notice it takes 3 and a little bit radians to complete half the circle. This "3 and a little bit," is of course exactly π.
Knowing this, there are a few equivalencies to remember:
π/6 = 30°
π/4 = 45°
π = 180°
3π/2 = 270°
2π = 360°
And finally, to convert between degrees and radians:
deg->rad: (π/180) * deg
rad->deg: (180/π) * rad
Previously on Sinewave: Pantsless:
"The period of this function is two-pi radians! What are we going to do!?""Our Subtraction Ray is no use! It fluctuates between 1 and -1!"
Imagine we have a sprite moving back and forth, perhaps with the following code:
(please forgive my lack of SPANIM for basic motion)
SPSET 0,84
VAR X, Y = 120, DX = 5
WHILE 1
IF X <= 0 THEN
DX=2.5
ELSEIF X >= 400 THEN
DX=-2.5
ENDIF
X = X + DX
SPOFS 0, X, Y
VSYNC
WEND
Here, the letter T bounces back and forth between the edges of the screen.
However, this can be very awkward for actual game situations: imagine if this were supposed to model a bouncing ball. That wouldn't work at all.
We can use SIN to ease the interpolation between the values, but first we'll need a general interpolation function:
'Linear interpolation (map t[0,1] to [min,max])
DEF LERP(T, MIN, MAX)
RETURN MIN + T * (MAX - MIN)
END
This function takes a percent value (0.00 to 1.00) and maps it to a position along the line from MIN to MAX. That means that anything that produces a value between 0 and 1 can be used for input... including the trigonometric functions.
lerp constructs a line like so, such that Y(0.0)=min and Y(1.0)=max. The parameter t is a position along the line. 50% is exactly halfway between min and max. However, lerp does nothing to RESTRICT the output to only the positions between 0% and 100%: you could continue on this line in any direction. 200% is simply min+2max.
Actually, it just makes it a little easier on us when mapping the sine values to actual positions. As we'll see in later sections, it isn't actually necessary, either.
Let's do that, then:
SPSET 0,84
SPSET 1,88
VAR X, Y = 100, X2, Y2 = 140, DX, DX2
WHILE 1
'first sprite
IF X <= 0 THEN
DX=2.5
ELSEIF X >= 384 THEN
DX=-2.5
ENDIF
X = X + DX
SPOFS 0, X, Y
'second sprite
IF X2 <= 0 THEN
DX2=2.5
ELSEIF X2 >= 384 THEN
DX2=-2.5
ENDIF
X2 = X2 + DX2
SPOFS 1, LERP(SIN(PI() * X2 / 384 ), 0, 384), Y2
VSYNC
WEND
'Linear interpolation (map t[0,1] to [min,max])
DEF LERP(T, MIN, MAX)
RETURN MIN + T * (MAX - MIN)
END
In this sample, I've added a second sprite, which is controlled as the first one, with the exception of using LERP(SIN(PI() * X2 / 384 ), 0, 384) in place of X. Also, I changed all the "400" values to "384," just so that the 16x16 sprites stay on screen.
So that's neat: we get this bouncing effect on it that looks pretty natural. You'll also notice that it moves exactly twice as fast as the other one. Time to play around with a simplified demo.
First off, since sine is periodic, we don't actually need the "IF X2 <= 0..." check, assuming we want the motion to loop forever.
SPSET 0,88
VAR X, Y = 120, DX = 2.5
WHILE 1
X = X + DX
SPOFS 0, LERP(SIN(PI()*X/384), 0, 384), Y
VSYNC
WEND
'Linear interpolation (map t[0,1] to [min,max])
DEF LERP(T, MIN, MAX)
RETURN MIN + T * (MAX - MIN)
END
This code, however, does NOT have the same effect as the earlier sample!
The first never allowed π * x / 384 to exceed π: X remained between 0 and 384, and thus PI()*X/384 remained between 0 and π. As a result, SIN() always returned positive values (that's why it completed two bounces for each one by the other sprite). Now that X is unbounded, SIN() will continue into the negative values before coming "back up." Since a line can be 'followed' in the negative direction, LERP returns a negative value after multiplying, as well.
Now, if we wanted to get the bouncing effect again, we could simply take the absolute value of SIN before passing it to LERP, but that's not fun. Instead, we'll use this property (as well as that consequence of our linear function) to get easing both in and out.
SPSET 0,88
VAR X = 0, Y = 120, DX = 2.5
WHILE 1
X = X + DX
SPOFS 0, LERP(SIN(PI()*X/400), 192, 384), Y
VSYNC
WEND
'Linear interpolation (map t[0,1] to [min,max])
DEF LERP(T, MIN, MAX)
RETURN MIN + T * (MAX - MIN)
END
That's about as much as I know. Figuring out how to apply it is up to you, but there's one more note within the scope of this tutorial:
The graph of cosine is phase-shifted π/2 from the graph of sine:
That means at x=0, cos(0) = 1, where on the other hand sin(0) = 0.
Consider this when choosing a function for the start position of an animation (though you could, of course, just shift by +π/2 yourself)
Transformations (With Remilia the Vampire)
The general form of sine is Asin((2πk)x + c) + h< to be continded
Sorry for crappy tutorial. If I forgot anything leave a comment and I might be able to fill it in.
15 Comment(s)ProKukuAvatar TabooI didn't change my avatar for 180 daysWebsiteFinal(?) Contest Runner UpI placed 2nd in the (possibly) final SmileBASIC Source Contest! October 2018Programming ContestCthulhuJoin the cultEaster Eggs“< to be continded”
*six months later* HmmmY_ackHead AdminI kept beginning to write and then discarding the drafts.
I'm not really even sure how best to present this any more... transformations in particular are better to be interactive.
I haven't forgotten about it, though.kenchicFirst YearMy account is over 1 year oldWebsiteBeginner ProgrammerI'm just starting out! I'm still trying to grasp the basics.Programming Strengthosu! Is Awesome!I love osu!Express Yourself6 months later
(has forgotten)MinxrodThird YearMy account is over 3 years oldWebsiteExpert ProgrammerProgramming no longer gives me any trouble. Come to me for help, if you like!Programming StrengthVideo GamesI like to play video games!Hobbies"trigonometry is a scary word"
What? Trigonometry isn't a scary word.
Other than that good tutorial probably, I only skimmed it. :P
Y_ackHead AdminAny word with more than three syllables can be effectively used to deter small children and rodents.snail_HelperReceived for being very helpful around SmileBASIC SourceAchievementsOSP Contest 2 Contest ParticipantI participated in the second SmileBASIC Source OSP Contest!Night PersonI like the quiet night and sleep late.Express Yourselfi love the unit circlekenchicFirst YearMy account is over 1 year oldWebsiteBeginner ProgrammerI'm just starting out! I'm still trying to grasp the basics.Programming Strengthosu! Is Awesome!I love osu!Express YourselfI learned about the magic of the unit circle a few weeks back when I finally decided to see what was with those sin and cos functions.
I can't really say that I'll ever learn it more effectively in school. Math teachers change the definitions to fit their curriculum, and it totally messed me up on the dang homework they assigned me this night.JohnCorbyi accidentally learned all this from a random program i found a while agomystman12First DayJoined on the very first day of SmileBASIC SourceWebsiteIntermediate ProgrammerI can make programs, but I still have trouble here and there. Programming StrengthDeep SleepHiddenWebsiteHmm, this'll be a nice refresher if I need it, thanks! I'm actually finishing a pre-calc class right now so all this is still fresh in my mind.spaceturtlesVideo GamesI like to play video games!HobbiesAvatar BlockI didn't change my avatar for 30 days.WebsiteIntermediate ProgrammerI can make programs, but I still have trouble here and there. Programming Strength"to be continded"12Me21Head AdminThird YearMy account is over 3 years oldWebsiteSyntax HighlighterReceived for creating the code syntax highlighter on SBSNight PersonI like the quiet night and sleep late.Express Yourself?COS(RAD(270))
-0MasterR3C0RDHelperReceived for being very helpful around SmileBASIC SourceAchievementsThird YearMy account is over 3 years oldWebsiteosu! Is Awesome!I love osu!Express YourselfSeemsGoodSpiderLilyAvatar BlockI didn't change my avatar for 30 days.WebsiteAvatar EmbargoI didn't change my avatar for 90 daysWebsitenice pic at the end thereMochaProbablyExpert ProgrammerProgramming no longer gives me any trouble. Come to me for help, if you like!Programming StrengthNight PersonI like the quiet night and sleep late.Express YourselfDrawingI like to draw!Hobbiesbut how could we use sine to create smooth animationskenchicFirst YearMy account is over 1 year oldWebsiteBeginner ProgrammerI'm just starting out! I'm still trying to grasp the basics.Programming Strengthosu! Is Awesome!I love osu!Express YourselfSPROT 0,SIN(MAINCNT/100)*20
You happy now?
?COS(RAD(270)) -0
SPROT 0,SIN(MAINCNT/100)*20
You happy now?