Plotting angles in Object Pascal
From time to time someone hears that I’m a software engineer, and immediately they add to the conversation “oh you must be so good at mathematics, I suck at Maths”. I wish! In fact my math skills are ordinary at best. Like most creative individuals I tend to only learn something when I really need it. And sadly this was a personality trait even in highschool so I had a hard time surviving Math classes (or the teachers had a hard time dealing with me is more precise, I got expelled for beating up two of them).
The result is that whenever I face a trigonometry problem, I really am the mercy of the internet and friends. So I have decided to do something about this. I mean, I am 41 years of age after all. So off I go to Amazon.com to order a “trig for dummies” book, as I scavenger my son’s math curriculum for clues 🙂
From what I understand, programmers seem to be divided in two groups; The first group believes angular calculations is a waste of time and should be avoided if you can, because interpolation delivers more or less the same results but at a fraction (pun intended) of the cpu cost. The second group like to apply “real world maths” onto the flat landscape of Our cardassian coordinate system, which costs more but tend to deliver accurate results down to the last pixel.
The problem for a person who doesn’t really know that much about trigonometry, is that: if you don’t know how to do it properly, then you can’t possibly know how to derive an alternative. In other words, how can I write code which yields the same results if I have no clue about either Method?
Oh, and I’m on a new PC which uppercases every single word by force, so if this post looks a mess – that’s why. Windows 8.1 is just the gift that keeps on giving..
Hands on stuff
Imagine for a second that you want to create a shooting game. You have a top-down view of a battlefield and some mini-soldiers running around at your command. Whenever you click an enemy on the map, your soldiers should attempt to get within range of the enemy, and then start shooting. But that doesnt mean every single shot actually hit’s the target.
Now shooting “in a direction” is not as easy as it sounds. If your soldier is positioned at 100,100 on-screen – and your target is at 260,49 “or there about”, it wouldn’t be much of a game if your soldier hits the target every single time. Also, the enemy soldier will be moving away from the hit-zone and shoot back.
Another thing to consider is the strength of the weapon. The bullet should, when missing it’s target, continue on its trajectory. It doesn’t fall dead to the floor when it reaches the x2,y2 region of the map.
So how do we calculate such a shot?
Well, in my head the first thing that came up was:
- Find the angle at which the enemy is positioned (pt2) from the soldier (pt1)
- Calculate a path from p1 THROUGH p2, continuing forever; Affected by speed, wind, resistance and the size of the map
Naturally the calculation of the path should not be “pre calculated”, that was just the first way for me to approach the problem. All games have a main-loop and each element on the screen is only slightly updated per-frame. When this happens very fast the game comes to life.
Using interpolation means that you update the position of the bullet by fractions. More or less dividing the total difference between start and stop into smaller “chunks” –and then iterating through these steps until you reach the goal. This allows us to calculate an array of TPointF (floating point x,y record) which goes from start-position towards the end-position. Should it miss the target it will simply continue past the original target co-ordinate and ultimately go out of sight. At which point the game-engine can choose to either just drop the sprite object all together, or allow it to continue without being rendered until it’s velocity results in a halt (or you hit something else). At which point it should also be eliminated from the update-loop).
This would be a quick and dirty interpolated Version (thanks for the example Primoz!):
function TForm1.BuildList(x1,y1,x2,y2:float):Array of TPointF; var x: Integer; mlen: Integer; begin (* Single point? *) if (x1=x2) and (y1=y2) then begin result.add(TPointF.Create(x1,y1)); end else begin (* Add start point *) result.add(TPointF.Create(x1,y1)); (* Calculate axis length. Note: This controls the amount of distance you expand from the original position towards the target *) mlen:= round( (abs(x1-x2) + abs(y1-y2)) ); (* calculate mid-part *) for x:=1 to mLen do result.add(TpointF.Create ( x1 + x/100 * (x2-x1), y1 + x/100 * (y2-y1) )); (* Add end-point *) result.add(TPointF.Create(x2,y2)); end; end;
Please note that this is just quick and dirty code. It pre-calculates a given path from X1,Y1 through X2,y2; which is not something you want to use in a game. There you want to have fast and small calculations per item which is repeated in sync With the screen-redraw.
But this solves my problem quite elegantly: being able to derive a position through a general direction.
Angles are different, here I can use the initial idea on how to solve it: namely to first get the angle Pt2 is in conjunction With Pt1 – and then calculate a path through that angle.
function AngleOfLine(const P1, P2: TPointF): Double; begin Result := RadToDeg(ArcTan2((P2.Y - P1.Y),(P2.X - P1.X))); if Result < 0 then Result += 360; end; function TForm1.BuildList(x1,y1,x2,y2:float):Array of TPointF; var Angle: Float; start,stop: TPointF; x: Integer; begin //build and add starting point start:=TPointF.Create(x1,y1); result.add(start); //build end-point record stop:=TPointF.Create(x2,y2); //Get angle of PT2 in context With PT1 Angle:=AngleOfLine(start,stop); //Generate 100 Points along angle for x:=1 to 100 do begin X2 := X1 + X * COS(DegToRad(Angle)); Y2 := Y1 + X * SIN(DegToRad(Angle)); result.add(TpointF.Create(x2,y2)); end; result.add(stop); end;
Right, with those two Experiments behind me, I took the time to extract what I have learned into a record which represents a position. This record has functions which allows you to update the position using a next() mechanism – so you can quickly update a bunch of bullets in a main-game-loop.
This allows us to not only keep track of the position, but also to calculate the next step with a single call. Turning this into a class, or using it as a property of a “bullet” class should be easy enough later.
type TQTXPosition = record Start: TPointF; //Where the bullet is shot from Target: TPointF; //Where the bullet is shot towards Position: TPointF; //Current position of bullet Iterator: Integer; //iterator value DistanceToTarget: Float; //Approx distance between start and target function Beyond:Boolean; function Next:TPointF; function Create(x1,y1,x2,y2:Float):TQTXPosition;overload; function Create(aStart,aTarget:TPointF):TQTXPosition;overload; end; //############################################################################# // TQTXPosition //############################################################################# function TQTXPosition.Create(x1,y1,x2,y2:Float):TQTXPosition; begin result.start.x:=x1; result.start.y:=y1; result.target.x:=x2; result.target.y:=y2; result.iterator:=0; result.Position.X:=0; result.Position.Y:=0; result.DistanceToTarget:= round(( abs(result.start.x-result.target.x) + abs(result.start.y-result.target.y))); end; function TQTXPosition.Create(aStart,aTarget:TPointF):TQTXPosition; begin result.start:=aStart; result.target:=aTarget; result.iterator:=0; result.Position.X:=0; result.Position.Y:=0; result.DistanceToTarget:= round(( abs(result.start.x-result.target.x) + abs(result.start.y-result.target.y))); end; function TQTXPosition.Beyond:Boolean; begin result:=Iterator > DistanceToTarget; end; function TQTXPosition.Next:TPointF; begin inc(Iterator); Position.X:= Start.x + Iterator/100 * (Target.x-Start.x); Position.Y:= Start.y + Iterator/100 * (Target.y-Start.y); result:=Position; end;
The “NeXT” function simply adds 1 to the iterating value, calculates the NeXT position, and Returns it. You may want to place this in a real class to make it more smooth to work With. The constructors allows you to define where to start and the target aim. The Beyond() function Returns true if the bullet has passed the target aim, and the DistanceToTarget Field represents the approx distance from start to aim.
Again, this is quick and dirty stuff – but for a novice in trig’ like myself it’s easier to understand the idea of “angle” as “slant towards” and radians as “reaching out towards a circumference”. Well, I just ordered my “for dummies” book and will hopefully get this under wraps. Special thanks to Eric, Primoz and Jørn for giving me extremely simple starter pointers and one-liner examples of various topics. Suddenly what used to be very booring at school is becoming exciting and useful as an adult 🙂
Never to old to learn new stuff!
Vector math’s is what seems to be the best way to solve most of this, so that’s on my list.
Tip for teachers
There will always be young boys that hates theory. Most of these kids are visual and tend to approach problems by “seeing” them mentally, and as such pure theory is regarded (or experienced) as the most boring thing in the universe. But the same kids, often creative and inventive, loves video games!
So my tip for teachers who have students like that (or as wild as I was) is to try to find a context for knowledge that these kids can find useful. Had someone told me in school that I could use this to create games – I would have consumed the books as fast as i could, because coding games and demo’s was something I really loved doing.
In fact, co-sinus and sinus was something I got to grips with when coding a “sinus scroll text” at early high-school. But like all kids I was under the impression that adults had it wrong and we had nothing to learn from them.
So if you are a teacher, try to make math’s more interesting by connecting it with computer games and coding (if the pupil responds to that). Who knows, one of your worst pupils may turn out to be the one who actually uses what you taught decades lates 😉