Spell presents: ////==\\\\ || || //======== |\ /| //\\ //======\ || || || || ||\ /|| // \\ // || || || || || \ / || // \\ || || ||======|| ||==== || \/ || || || || || || || || || || ||====|| || ==\\ || || || || || || || || \\ || || || || \\======== || || || || \\====// Issue 10 14-9-96 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- Index: 1. Introduction 1.1. About the magazine 1.2. About the author 1.3. Distribution 1.4. Subscribing 1.5. Contribuitions 1.6. Hellos and greets 2. Putting order into chaos 2.1. Introduction 2.2. BubbleSort 2.3. ShellSort 2.4. QuickSort 3. The fire effect 4. Designing a text adventure - Part III 4.1. Creating objects 4.2. Seeing the objects 4.3. Interacting with objects 5. The wonderfull world of scrolies ! 5.1. Introduction 5.2. Simple scroller 5.3. Sine scroller 5.4. Mirror and water scroller 6. First steps into virtual reality 6.1. Wireframe graphics 6.2. Basic transformations 6.3. A 3D starfield 6.4. VectorBalls 7. Sprites Part III - Lots of fun stuff 7.1. Transparency 7.2. Moving over a background 7.3. Flipping a sprite 8. Graphics Part IX - Polygons 8.1. Simple polygons 8.2. Filled polygons 8.3. Ellipses and Arcs 8.4. Filled Ellipses 9. Hints and tips 10. Points of view 11. The adventures of Spellcaster, part 10 -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 1. Introduction 1.1. About the magazine Welcome to super-special issue number 10 !! Why is this issue so special ? Well, because it's number 10... I never thought I would get this far, and I almost gave up on the 'The Mag' project (see large delay between issues 5 and 6). But, here it is, to last... :) As usual, this is brought by Spellcaster, alias known as Diogo de Andrade. What do we have this issue ? Let me see... An article by Scorpio, about the fire effect. In the graphics section, I'll talk about polygons and other draw tools (I know I said assembler, but I'll leave that for next issue). In the Sprites section, I'll talk about transparency, bitmap rotation and moving over a background. In 'Designing a text adventure', I'll write about objects and object interaction. I'll also do an article on 3D basic transformation and some simple effects. Also, an article on different kinds of scrollies. On the agenda, I also have an article on sorting... You should be reading this issue in the first of the SpellUtilities, SpellView ! It's a program designed to view text with some features, like colors and other neat stuff... :) You probably can get it from the same place you got this issue, but if you can't, look in the BBS's listed somewhere in this issue, or look in my HomePage... It includes full source code, so you can change it, and see how it was done... :) This can only be probably seen with SpellView V1.4... Get it... It's better, bugfixed, etc... This magazine is dedicated to all the programmers and would-be programmers out there, especially to those that can't access the Net easily to get valuable information, to those who wish to learn how to program anything, from demos to games, passing through utilities and all sort of thing your mind can think of, and to those that can't find the right information. When you read this magazine, I'll assume some things. First, I assume you have Borland's Turbo Pascal, version 6 and upwards (and TASM for the assembly tutorials). I'll also think you have a 80386 (or 386 for short; a 486 would be even better), a load of patience and a sense of humor. This last is almost essencial, because I don't receive any money for doing this, so I must have fun doing it. I will also take for certain you have the 9th grade (or equivelent). Finally, I will assume that you have the last issues of 'The Mag', and that you have grasped the concepts I tried to transmit. If you don't have the issues, you can get them by mail, writing to one of the adresses shown below (Snail mail and Email). As I stated above, this magazine will be made especially for those who don't know where to get information, or want it all in the same place, and to those who want to learn how to program, so I'll try to build knowledge, building up your skills issue by issue. If you sometimes fail to grasp some concept, don't despair; try to work it out. That's what I did... Almost everything I know was learnt from painfull experience. If you re-re-re-read the article, and still can't understand it, just drop a line, by mail, or just plain forget it. Most of the things I try to teach here aren't linked to each other (unless I say so), so if you don't understand something, skip it and go back to it some weeks later. It should be clearer for you then. Likewise, if you see any terms or words you don't understand, follow the same measures as before. Ok, as I'm earing the Net gurus and other god-like creatures talking already, I'm just going to explain why I use Pascal. For starters, Pascal is a very good language, ideal for the beginner, like BASIC (yech!), but it's powerfull enough to make top-notch programms. Also, I'll will be using assembly language in later issues, and Pascal makes it so EASY to use. Finally, if you don't like my choice of language, you can stop whining. The teory behind each article is very simple, and common with any of the main languages (C, C++, Assembly - Yes, that's true... BASIC isn't a decent language). Just one last thing... The final part of the magazine is a little story made up by my distorted mind. It's just a little humor I like to write, and it hasn't got nothing to do with programming (well, it has a little), but, as I said before, I just like to write it. 1.2. About the author Ok, so I'm a little egocentric, but tell me... If you had the trouble of writing hundreds of lines, wouldn't you like someone to know you, even by name ? My name is Diogo de Andrade, alias Spellcaster, and I'm the creator, editor and writer of this magazine. I live in a small town called Setubal, just near Lisbon, the capital of Portugal... If you don't know where it is, get an encyclopedia, and look for Europe. Then, look for Spain. Next to it, there's Portugal, and Setubal is in the middle. I'm 18 years old, and I just made it in to the university (if you do want to know, I'm in the Technical Institute of Lisbon, Portugal), so I'm not a God-Like creature, with dozens of years of practice (I only program by eight years now, and I started in a Spectrum, progressing later to an Amiga. I only program in the PC for a year or so), with a mega-computer (I own a 386SX, 16 Mhz), that wear glasses with lens that look like the bottom of a bottle (I use glasses, but only sometimes), that has his head bigger than a pumpkin (I have a normal sized head) and with an IQ of over 220 (mine is actually something like 180-190). I can program in C, C++, Pascal, Assembly and even BASIC (yech!). So, if I am a normal person, why do I spend time writing this ? Well, because I have the insane urge to write thousands of words every now and then, and while I'm at it, I may do something productive, like teaching someone. I may be young, but I know a lot about computers (how humble I am; I know, modesty isn't one of my qualities). Just one more thing, if you ever program anything, please send to me... I would love to see some work you got, maybe I could learn something with it. Also, give me a greet in your program/game/demo... I love seeing my name. 1.3. Distribution I don't really know when can I do another issue, so, there isn't a fixed space of time between two issues. General rule, I will try to do one every month, maybe more, probably less (Eheheheh). This is getting to an issue every two months, so, I'll think I'll change the above text... :) 'The Mag' is available by the following means: - Snail Mail : My address is below, in the Contributions seccion... Just send me a disk and tell me what issues you want, and I will send you them... - E-Mail : If you E-mail me and ask me for some issues, I will Email you back with the relevant issues attached. - Internet : You can access the Spellcaster page and take the issues out of there in: http://alfa.ist.utl.pt/~l42686 Follow the docs link... - Anonymous ftp : I've put this issue of 'The Mag' on the ftp.cdrom.com site... I don't know if they'll accept it there, because that's a demo only site, and my mag doesn't cover only demos, but anyways, try it out... It has lots of source code of demos. - BBS's : You can check out the BBS's list that will carry this and all the other issues of 'The Mag' in the file SPELL.DOC. 1.4. Subscribing If you want, I'm starting "The Mag"'s subscription list... To get 'The Mag' by Email every month, you just need to mail me and tell me so... Then, you will receive it every time a new issue is made... 1.5. Contributions I as I stated before, I'm not a God... I do make mistakes, and I don't have (always) the best way of doing things. So, if you think you've spotted an error, or you have thought of a better way of doing things, let me know. I'll be happy to receive anything, even if it is just mail saying 'Keep it up'. As all human beings, I need incentive. Also, if you do like to write, please do... Send in articles, they will be welcome, and you will have the chance to see your names up in lights. They can be about anything, for a review of a book or program that can help a programmer, to a point of view or a moan. I'm specially interested in articles explaining XMS, EMS, DMA and Soundblaster/GUS. If anyone out there has a question or wants to see an article about something in particular, feel free to write... All letters will be answered, provided you give me your address. If you have a BBS and you want it to include this magazine, feel free to write me... I don't have a modem, so I can only send you 'The Mag' by Email. You can also contact me personally, if you study on the IST (if you don't know what the IST is, you don't study there). I'm the freshman with the black hair and dark-brown eyes... Yes, the one that in all talkers at the same time... :) My adress is: Praceta Carlos Manito Torres, n§4/6§C 2900 Set£bal Portugal Email: l42686@alfa.ist.utl.pt And if you want to contact me on the lighter side, get into the Lost Eden talker... To do that telnet to: Alfa.Ist.Utl.Pt : Port 1414 If that server is down, try the Cital talker, in Zeus.Ci.Ua.PT : Port 6969 I'm almost always there in the afternoon... As you may have guessed already, my handle is Spellcaster (I wonder why...)... 1.6. Hellos and greets I'll say hellos and thanks to all my friend, especially for those who put up with my not-so-constant whining. Special greets go to Scorpio, Denthor from Asphyxia (for the excelent VGA trainers), Draeden from VLA (for assembly tutorials), Dr.Shadow (Delta Team is still up), Joao Neves for sugestions, testing and BBS services, Garfield (for the 'The Mag' logo and general suport) and all the demo groups out there. I will also send greets to everybody that responded to my mag... Thank you very much ! -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 2. Putting order into chaos 2.1. Introduction Welcome to another article by your friend Spellcaster... This article is about sorting an array... What's sorting, you may ask ? [ Everyone asks what's sorting !! ] Well, sorting is organizing data in a data structure... Get it ? [ Everybody says no... ] Ok, imagine you have an array like this: A[1]:=221; A[2]:=321; A[3]:=831; A[4]:=432; A[5]:=721; Sorting is converting that array into: A[1]:=221; A[2]:=321; A[3]:=432; A[4]:=721; A[5]:=831; See ? Sorting is the same as ordering... There are _LOTS_ of methods for doing this, but there are some that are 'standart'... Each one has it's advantages and disadvantages... I'll talk about three methods... I'll start with: 2.2. BubbleSort BubbleSort is the slowest and easiest to program method, and it is sometimes the best one for small and desordered arrays. The ideia of BubbleSort is to order an array by swapping the contents of two adjacent memory positions (if they are out of order). A flag assures that the array is ordered. Execution example: We want to sort the array [ 7 4 2 3 5 1 6 ]. Let's see: ÚÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Step ³ Array ³ Comment ³ ÃÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ 0 ³ 7 4 2 3 5 1 6 ³ The beggining ³ ³ 1 ³ 4 7 2 3 5 1 6 ³ Because 4<7, then 4 and 7 are exchanged ³ ³ 2 ³ 4 2 7 3 5 1 6 ³ Because 2<7, then 2 and 7 are exchanged ³ ³ 3 ³ 4 2 3 7 5 1 6 ³ Because 3<7, then 3 and 7 are exchanged ³ ³ 4 ³ 4 2 3 5 7 1 6 ³ Because 5<7, then 5 and 7 are exchanged ³ ³ 5 ³ 4 2 3 5 1 7 6 ³ Because 1<7, then 1 and 7 are exchanged ³ ³ 6 ³ 4 2 3 5 1 6 7 ³ Because 6<7, then 6 and 7 are exchanged ³ ³ ³ ³ As the array isn't ordered yet, the process³ ³ ³ ³ restarts: ³ ³ 7 ³ 2 4 3 5 1 6 7 ³ Because 2<4, then 2 and 4 are exchanged ³ ³ 8 ³ 2 3 4 5 1 6 7 ³ Because 3<4, then 3 and 4 are exchanged ³ ³ 9 ³ 2 3 4 5 1 6 7 ³ Because 5>4, the array remains the same ³ ³ 10 ³ 2 3 4 1 5 6 7 ³ Because 1<5, then 1 and 5 are exchanged ³ ³ .. ³ ............. ³ ......................................... ³ ³ ³ 1 2 3 4 5 6 7 ³ ³ ÀÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ You should get the ideia by now... So, let's do a procedure that implements the above explained algorithm. The example program orders an array with 40 random inserted elements, generated from a seed (read this issue's hints and tips). The program also keeps tracks on how many comparissons and exchanges it does, so we can compare this type of sort with the others, because that's the way to evaluate the algorithm's overall eficiency. So, without further due, here's the code: Program BubbleSort; Uses Crt; Type DataType=Array[1..40] Of Integer; Var Data:DataType; A:Byte; Compare:Word; XChange:Word; Procedure FillData; Begin RandSeed:=122217; { Random seed } For A:=1 To 40 Do Data[A]:=Random(1000); End; Procedure WriteArray; Var B:Byte; Begin For A:=0 To 3 Do Begin For B:=1 To 10 Do Begin GotoXY(B*6+5,A+10); Write(Data[A*10+B]); End; Writeln; End; End; Procedure Sort; Var Flag:Boolean; C:Integer; Begin Compare:=0; XChange:=0; Repeat Flag:=True; For A:=1 To 39 Do Begin If Data[A]>Data[A+1] Then Begin { Exchange data } C:=Data[A]; Data[A]:=Data[A+1]; Data[A+1]:=C; Flag:=False; { Increase exchanges counter } Inc(XChange); End; { Increase comparisons counter } Inc(Compare); End; Until Flag; End; Begin Clrscr; FillData; GotoXY(13,8); Writeln('Array before BubbleSort:'); WriteArray; ReadLn; Sort; Clrscr; GotoXY(13,8); WriteLn('Array after BubbleSort:'); WriteArray; GotoXY(13,16); WriteLn('Compares=',Compare); GotoXY(13,17); Writeln('Exchanges=',XChange); ReadLn; End. Well, this is the easiest kind of sort... Let's move on to the next: 2.3. ShellSort ShellSort is the most complex (for me, at least) way of sorting... The problem with BubbleSort is that it exchanges values to near to each other... If you check the example array [ 7 4 2 3 5 1 6 ], notice that the number 7 must traverse almost the whole array to reach it's final position. Wouldn't it be great to make it go to it's position in 2 steps, instead of the 6 it requires ? Well, the ideia of ShellSort is to have an incremental factor... In reality, ShellSort is a special case of BubbleSort, a case where the elements that are compared/exchanged aren't so near to each other. The number of comparissons will sometimes be the same or bigger than BubbleSort, but the number of exchanges will be less... So, let's see an example: The increment selected is 4: ÚÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Step ³ Array ³ Comment ³ ÃÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ 0 ³ 7 4 2 3 5 1 6 ³ The beggining ³ ³ 1 ³ 5 4 2 3 7 1 6 ³ Because 5<7, 5 and 7 are exchanged ³ ³ 2 ³ 5 1 2 3 7 4 6 ³ Because 1<4, 1 and 4 are exchanged ³ ³ 3 ³ 5 1 2 3 7 4 6 ³ Because 2<6, the array remains unaltered ³ ÀÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ If you look at the above example and think 'This demonstration isn't ended !!', you are wrong... This demonstration is more than finished. ShellSort requires several diferent increments, finalizing with the increment of 1. So, let's now adapt the array we've obtained with ShellSort with increment of 2: ÚÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Step ³ Array ³ Comment ³ ÃÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ 0 ³ 5 1 2 3 7 4 6 ³ The beggining ³ ³ 1 ³ 2 1 5 3 7 4 6 ³ Because 2<5, 2 and 5 are exchanged ³ ³ 2 ³ 2 1 5 3 7 4 6 ³ Because 1<3, the array remains unaltered ³ ³ 3 ³ 2 1 5 3 7 4 6 ³ Because 5<7, the array remains unaltered ³ ³ 4 ³ 2 1 5 3 7 4 6 ³ Because 3<4, the array remains unaltered ³ ³ 5 ³ 2 1 5 3 6 4 7 ³ Because 6<7, 6 and 7 are exchenged ³ ÀÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Well, the array isn't ordered already, but notice that the number 7 is already in the correct position... Now, you should apply ShellSort with increment of 1 to the array obtained from the previous ShellSorts. That is equal of applying BubbleSort. So, let's check out an example program... Again it keeps tracks of the numbers of comparissons and exchanges made: Program ShellSort; Uses Crt; Type DataType=Array[1..40] Of Integer; Var Data:DataType; A:Byte; Compare:Word; XChange:Word; Procedure FillData; Begin RandSeed:=122217; { Random seed } For A:=1 To 40 Do Data[A]:=Random(1000); End; Procedure WriteArray; Var B:Byte; Begin For A:=0 To 3 Do Begin For B:=1 To 10 Do Begin GotoXY(B*6+5,A+10); Write(Data[A*10+B]); End; Writeln; End; End; Procedure Sort(H:Integer); Var Flag:Boolean; B,C:Integer; Begin For A:=1+H To 40 Do Begin C:=Data[A]; B:=A; While (B-H>0) And (Data[B-H]>C) Do Begin Data[B]:=Data[B-H]; B:=B-H; Inc(Compare,2); Inc(XChange); End; Data[B]:=C; End; End; Begin Clrscr; FillData; GotoXY(13,8); Writeln('Array before ShellSort:'); WriteArray; ReadLn; Compare:=0; XChange:=0; Sort(4); Clrscr; GotoXY(13,8); WriteLn('Array after ShellSort (increment 4):'); WriteArray; ReadLn; Clrscr; Sort(2); Clrscr; GotoXY(13,8); WriteLn('Array after ShellSort (increment 2):'); WriteArray; ReadLn; Clrscr; Sort(1); Clrscr; GotoXY(13,8); WriteLn('Array after ShellSort (increment 1):'); WriteArray; GotoXY(13,16); WriteLn('Compares=',Compare); GotoXY(13,17); Writeln('Exchanges=',XChange); ReadLn; End. Just compare the difference between the two sorts... If you are a little confused by the example program, reread it, keeping in mind that: 1) The program goes from end to beggining of the array 2) The program finds the best position for one value. For example: Original Array: [ 7 2 3 1 5 4 ] If you use the original algorithm I talked about, then the array would look like this after a step (increment 2): [ 3 2 7 1 5 4 ] While the new algorithm makes the array like this: [ 3 2 5 1 7 4 ] In just one step, because it 'sticks' to number 7 until it can move it no further... Well, but what is the best sequence of increments ? There's just one rule for that... They shouldn't be multiples of each others... If it's a small array, that doesn't influenciate, but if it is a large array (i.e. more than 5000 positions), it does make a very big difference. This is lots of times faster than BubbleSort, but still it is slower than: 2.4. QuickSort This is harder to compreend for the begginer, because it is better done in a recursive form (see this issue's tricks and tips). So, QuickSort plays around with the ideia of partitioning the array is several smaller arrays, putting some order in each of the partitions. QuickSort doesn't really split the array in other arrays (that would need more memory). It just works with a part of the array at a time. To exemplify this one, I have to aquire another way to make the program's trace. So, assume that I and J as two variables that traverse the array, and imagine that the array we want to order is [ 44 55 12 42 94 18 6 67 ] So, the first thing to do in QuickSort is to select any number. Usually the number is in the array, altough this is not necessary. Let's choose the number 26. So, let's order the array in a way that all numbers smaller than 26 go to the beggining of the array (the left) and all the numbers that are bigger go to the end of the array (the right). So: Step 0: 44 55 12 42 94 18 06 67 ^ ^ I J So, what the algorithm does is to put I pointing to the first number it founds from the beggining of the array, larger than the select number (in this case 26). So, I points to 44. And we point J to the first number smaller than 26, counting from the right. In this case, J will point to 06. Then it switches the contents of both positions: Step 1: 06 55 12 42 94 18 44 67 ^ ^ I J And I and J find the next numbers than fullfill the caracteristics we've discussed previously. So, step after step: Step 2: 06 18 12 42 94 55 44 67 ^ ^ J I When J is smaller than I, that means that we have a 'final' array: [ 6 18 12 42 94 55 44 67 ] That we can think as two arrays: [ 6 18 12 ] + [ 42 94 55 44 67 ] One with elements smaller than 26 and the other with elements bigger than 26. So, we apply the same method to each one of the sub-arrays, until we reach subarrays with only one element. Then, if you 'sum' them all up, you'll get an ordered array. Check out the example program. In the example program, we'll choose the base number (the 26 in our previous example) as beeing the number halfway in the array (in the above example, it would be number 42 or 94): Program QuickSort; Uses Crt; Type DataType=Array[1..40] Of Integer; Var Data:DataType; A:Byte; Compare:Word; XChange:Word; Procedure FillData; Begin RandSeed:=122217; { Random seed } For A:=1 To 40 Do Data[A]:=Random(1000); End; Procedure WriteArray; Var B:Byte; Begin For A:=0 To 3 Do Begin For B:=1 To 10 Do Begin GotoXY(B*6+5,A+10); Write(Data[A*10+B]); End; Writeln; End; End; Procedure Sort; Var Flag:Boolean; I,J:Integer; X,N:Integer; Procedure SortSubArray(Left,Right:Byte); Begin { Partition } I:=Left; J:=Right; N:=Data[(Left+Right) Div 2]; Repeat { Find first number from the left to be < N } While Data[I] N } While Data[J]>N Do Begin Inc(Compare); Dec(J); End; { Exchange } If I<=J Then Begin X:=Data[J]; Data[J]:=Data[I]; Data[I]:=X; Inc(I); Dec(J); Inc(XChange); End; Inc(Compare,2); Until J Shovel - Needed to knock the door down -> Mask - Needed to pass the room with the gas -> Sword - Needed to kill the monsters -> Treasure - Guess ?! You can include other redundanct objects, without any purpose, except to confuse, or maybe amuse, the player... Let's put in the game 2 redundact objects: -> Lazer gun - A prop from a previous game -> A strange key - No purpose at all ! So, lets create the objects... 4.1. Creating objects To do so, we can use the program suplied (OBJGEN.PAS). It is very similar to the ROOMGEN.PAS program I gave with issue 8, except that this one creates rooms, while the OBJGEN.PAS program creates the objects. So, what do we need to store ? Well, it's name, it's position (we can move objects from a room to another, or be carrying it) and it's description... I'll explain the description part latter. So, let's define a record like this: Type ObjType=Record Name:String[80]; Pos:Byte; Desc:Array[1..5] Of String[80]; End; And let's define the number of objects in the game: Const NumberObjs=6; And, finally, let's define the array that will store the objects: Var Objects:Array[1..NumberObjs] of ObjType; So, after we've defined these data structures and made a file called OBJ.DAT (with the OBJGEN program) with the data, let's read the data, with a procedure similar in every respect with the ReadRoomData procedure that I gave in the first article of this series: Procedure ReadObjData; { Read from the disk the object data } Var F:Text; A,B:Byte; Flag:Boolean; Begin { Prepares the text file for accessing } Assign(F,'Obj.Dat'); Reset(F); { For every object in the game } For A:=1 To NumberObjs Do Begin { Read the name of the objects } ReadLn(F,Objects[A].Name); { Read the initial position of the objects } ReadLn(F,Objects[A].Pos); { Clear the object's description } For B:=1 To 5 Do Objects[A].Desc[B]:=''; { Read the description of the room } Flag:=True; B:=1; While Flag Do Begin ReadLn(F,Objects[A].Desc[B]); If (B=5) Or (Objects[A].Desc[B]='*') Then Flag:=False; Inc(B); End; End; Close(F); End; Just a note... If the POS field of the any object is equal to 0, that means that we are carrying the object, and if it is equal to 255, that means that the object is hidden (case of the gas mask, that only appears if the oven is open). 4.2. Seeing the objects An object is worthless, unless you can use it... And you can only use it if you can see it, to know it is there... So, there are two 'kinds' of objects you can see... Objects you own and objects that are lying around... A good game has different descriptions for both the 'kinds', but Fanglore just does the same thing in either case: spit out the description. So, there is a basic command that is equal in _EVERY_ text adventure in the world: EXAMINE ! The examine command has a parameter following, that is, it accepts (and requires) that you type another thing... That thing is the thing you want to examine... For example, if you wanted to examine the sword, you would type: EXAMINE SWORD So, EXAMINE is the command, and SWORD is the parameter. But notice that you aren't limited to just examine objects... You can examine anything in the game... For example, the oven in the kitchen... But I'll talk about that later, in another article of this serie... Also, notice that you can only examine objects that you have or that are in the same room as you... So, let's check out the EXAMINE procedure: Procedure Examine(Param:String); Var Flag:Boolean; A,B:Byte; Begin Flag:=True; { ***************************************************** } { Here we'll include code for things like the oven, etc } { ***************************************************** } { Search the object in the object list } A:=0; Repeat Inc(A); If Objects[A].Name=Param Then Flag:=True; Until Flag Or (A>NumberObjs); If Flag Then Begin { The object exists } { Check if it is in the room or in your possession } If (Objects[A].Pos=0) Or (Objects[A].Pos=CurrentRoom) Then Begin { The object is 'visible'... Write description } Writeln; B:=1; TextColor(Yellow); While (B<6) And (Objects[A].Desc[B]<>'*') Do Begin Writeln(Objects[A].Desc[B]); Inc(B); End; Writeln; End Else Begin { The object exists, but it's not visible } Writeln; TextColor(Yellow); WriteLn('I can''t see the ',Param,' here...'); WriteLn; End; End Else Begin { The specified parameter doesn't represent any existing } { object... } WriteLn; TextColor(Yellow); WriteLn('Sorry, but I don''t even know what is a ',Param); WriteLn; End; End; Of course you'll have to do the routine that calls this procedure... You must put it (as usual with all commands) in the Play procedure. The routine is like this: If Parsed[1]='EXAMINE' Then Begin Valid:=True; Examine(Parsed[2]); End; Simple, isn't it... But this is not over... If you remember what I told you about parsing, you know that the phrases are splitted in words. But, Gas Mask has two words in it's name... So, the program can't cope with objects with composite names. How can we make him accept it ? Well, we have to join the words again... Like this: If Parsed[1]='EXAMINE' Then Begin Valid:=True; { Join the words again } D:=3; E:=Parsed[2]; While Parsed[D]<>'' Do Begin E:=E+' '+Parsed[D]; Inc(D); End; Examine(E); End; You must define E as a string and D as a Byte types of variables. Altough this works with commands that only require one parammeter, this fails with other commands were multiple parameters are required... So, general rule, don't use objects with names that have spaces... But this section isn't finished yet... We still have to know what objects are in the room, and what objects do you carry... But I'll leave that for tomorrow, because I had 2 hours sleep last night... And I'm VERY tired... Good night... Zzzzzzzzzzzzzzzzzzzzzzzz.......... Ok... I'm back ! :) Ready for more... So... The problem this moment is knowing what are the objects we can interact. That's easy. When you enter a room, you make the program print out a list of visible objects. To do so, just add this code to the Look procedure: TextColor(LightCyan); Writeln('Visible objects:'); Flag:=False; For A:=1 To NumberObjs Do If Objects[A].Pos=RoomNumber Then Begin Writeln(' ',Objects[A].Name); Flag:=True; End; If Flag=False Then Writeln(' None'); Don't forget do define the Flag variable as a Boolean. It is used to know if there is any objects in the room... If it is false after the For loop ends, that means that there aren't any objects in the room, so the program writes down that it doesn't see any objects... To know what objects you are carrying, you can use second best-known command in text adventures... The INVENTORY command ! To do the inventory command, you just check all the objects in the game. If their position (the POS field) is equal to zero, then you are carrying it... Code: Procedure Inventory; Var A:Byte; Flag:Boolean; Begin Flag:=False; TextColor(LightBlue); WriteLn; WriteLn('Objects carried:'); For A:=1 To NumberObjs Do If Objects[A].Pos=0 Then Begin WriteLn(' ',Objects[A].Name); Flag:=True; End; If Flag=False Then Writeln(' None'); WriteLn; End; As usual, you have to add some code to the Play procedure, that calls the above procedure: If Parsed[1]='INVENTORY' Then Begin Valid:=True; Inventory; End; So, now you look at objects... You just have to do some interaction... 4.3. Interacting with objects An object is useless unless you can you interact somehow with it... There are three common interactions you can do with objects: Get, Drop and Use. Let's start with the easy ones: Get and Drop To get an object, all the program has to do is to set the POS field of the desired object to zero... After we verify if the object exists and if it is in the room, and if it isn't in your position already. To drop an object, you just have have to verify if you have that object and set the POS field of the object to the number of the room you're on... Coding wise: Procedure Get(O:String); Var A:Byte; Flag:Boolean; Begin Flag:=False; A:=0; Repeat Inc(A); If O=Objects[A].Name Then Flag:=True; Until (A>NumberObjs) Or (Flag=True); If Flag=True Then Begin { The object exists } If Objects[A].Pos=CurrentRoom Then Begin { The object is in the current room } { Get the object } Objects[A].Pos:=0; TextColor(LightCyan); WriteLn; WriteLn('You get the ',O); WriteLn; End Else Begin If Objects[A].Pos=0 Then Begin { The object is already in the possession } { of the player... } TextColor(LightCyan); WriteLn; WriteLn('You already have the ',O); WriteLn; End Else Begin WriteLn('You don''t see the ',O); WriteLn; End; End; End Else Begin { The object doesn't exist } WriteLn; TextColor(LightRed); Writeln('What are you talking about ?'); Writeln; End; End; As usual, you have to put a call to this procedure in the Play procedure... This command has the same problem as the examine command, so we must do the same thing that you did in the examine command: join the parsed array in a string that has the name of the object: If Parsed[1]='GET' Then Begin Valid:=True; { Join the words again } D:=3; E:=Parsed[2]; While Parsed[D]<>'' Do Begin E:=E+' '+Parsed[D]; Inc(D); End; Get(E); End; The drop procedure is similar, altough is a bit simpler: Procedure Drop(O:String); Var A:Byte; Flag:Boolean; Begin Flag:=False; A:=0; Repeat Inc(A); If (O=Upper(Objects[A].Name)) And (Objects[A].Pos=0) Then Flag:=True; Until (A>NumberObjs) Or (Flag=True); If Flag=True Then Begin { The object is in the player's possession } Objects[A].Pos:=CurrentRoom; TextColor(LightCyan); WriteLn; WriteLn('You drop the ',O); WriteLn; End Else Begin { The object doesn't exist or it isn't in the } { player's possession... } TextColor(LightRed); WriteLn; WriteLn('You don''t have the ',O); WriteLn; End; End; Again, add the following code to the Play procedure... You already know what it does: If Parsed[1]='DROP' Then Begin Valid:=True; { Join the words again } D:=3; E:=Parsed[2]; While Parsed[D]<>'' Do Begin E:=E+' '+Parsed[D]; Inc(D); End; Drop(E); End; So, Get and Drop are ready... Now, let's go to the third 'class' of object manipulation commands: The use commands. The use commands are lots of commands, but they all do the same thing: Manipulate the object. For example... You can use the Gas Mask by typing USE MASK... But let's do things harder... For example, USE MASK can be thought as breaking it! You are using it... So, let's do the WEAR command... :))) It is harder that way for the player know what to do... The uses commands are just lines and lines of IFs, because every object has a different effect. Some types of commands require more than one parameter. For example, to use the sword, you must type USE SWORD ON MONSTER... In FangLore we have two use commands... The USE command and the WEAR command. The wear command is only used for the gas mask... The use command is for the others. The theory is simple... According to the object you select, you must do a block of instruction that define what happens. For example, when you use the shovel with the door in room 20, the door to FangLore opens... Code for the WEAR command... This is so small and simple that we'll include it in the Play procedure. If Parsed[1]='WEAR' Then Begin { Join the words again } D:=3; E:=Parsed[2]; While Parsed[D]<>'' Do Begin E:=E+' '+Parsed[D]; Inc(D); End; If E='GAS MASK' Then Begin Valid:=True; { Check if the player has the gas mask } { We know that the mask is object number 2 } If Objects[2].Pos=0 Then Begin Mask:=True; TextColor(LightCyan); WriteLn; WriteLn('You wear the gas mask...'); WriteLn; End Else Begin TextColor(LightRed); WriteLn; WriteLn('You don''t have the gas mask.'); WriteLn; End; End; End; The Mask variable is a global variable of type Boolean. It should be initialized to False in the Init procedure. If it is true, that means that we are using it. We'll see later how that can be used... Now, let's move on to the USE command... There are the following possibilities: USE SHOVEL ON DOOR USE SWORD ON MONSTER The parser should eliminate the ON prenom... Do that with the EliminPrenoms procedure (see details in the first article of this serie). So, let's do the USE procedure: Procedure Use(Arg:ParsedType); Var A:Byte; Flag:Boolean; Begin Flag:=False; A:=0; Repeat Inc(A); If (Arg[2]=Upper(Objects[A].Name)) And (Objects[A].Pos=0) Then Flag:=True; Until (A>NumberObjs) Or (Flag=True); If Flag=True Then Begin { The object is "usable" } Flag:=False; { Check if player want to use the shovel } If Arg[2]='SHOVEL' Then Begin Flag:=True; { Check if it is used with the door } If Arg[3]='DOOR' Then Begin { Check if the player is in the proper } { location... } If CurrentRoom=20 Then Begin { The player is in the right place } { Open passage between the garden } { and the mansion... } Rooms[20].North:=15; TextColor(LightCyan); WriteLn; WriteLn('You knock the door down...'); WriteLn; End Else Begin { The player is someplace else } TextColor(LightRed); WriteLn; WriteLn('I don''t see a door...'); WriteLn; End; End Else Begin { The player didn't used the shovel with } { the door... } TextColor(LightRed); WriteLn; WriteLn('Use it with what ?!'); WriteLn; End; End; If Arg[2]='SWORD' Then Begin Flag:=True; { I'll include the code here in a future } { issue, when I'll talk about monsters... } End; End; If Flag=False Then Begin { No "legal" objects were used } TextColor(LightRed); WriteLn; WriteLn('Can't use that...'); WriteLn; End; End; We've must passe the entire parsed array as a parameter to the procedure, because objects like the shovel and the sword need another 'object' to work with... Remember that you are not limited to USE objects... You can use, for instance, a lever... It is not an object because you can't carry it around, but you can use it... But the code must be changed slightly... In the case of FangLore, we can only use objects... :) Notice also that in the case of the USE command, we can't use the trick we did with the Examine, Get, Drop and Wear commands, the trick of joining the parsed array together to form the name of the objects with more than one word (i.e. GAS MASK)... So, it is good to use as the name of the objects single words, or else lot's of manipulations are needed... In the case of FangLore there isn't a problem, but in other games it might be... Again, you must add the following code to the Play procedure: If Parsed[1]='USE' Then Begin Valid:=True; Use(Parsed); End; So, this article is finished... Next article of this serie will cover monsters and special rooms... Don't miss it !! :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 5. The wonderfull world of scrolies ! 5.1. Introduction Welcome to another article by your good (??) friend Spellcaster... This one is about scrollers... Or scrollies... :) So, what are they ? Scrollers are simply a piece of text that scrolls by the screen... Almost every demo in the world has at least one... There are billions of types of scrollers. They all revolve around the same principle... Memory... Scrollers are simple alterations of memory... If you move the memory the right way, you achieve the effect... Graphical scrollers are very similar to text scrollers... The only difference is that in text scrollers the text appears a character at a time, and in graphical scrollers, the text appears one collumn at a time... For example, if the font you are using is 16 pixels width, a character appears after 16 calls to the procedure that scrolls... Got it ? This article makes heavy use of the Font unit that is supplied with this issue... All the theory behind it was explained in last issue. Read the source code for more details... I've also included a color font,but is fairly limited... I hate drawing fonts, so I used one I've made for an old game of mine. Just remember one thing about the routine... Because of a small error in the conception of the font, the position in the array is not denoted as (x,y) but (y,x)... That is, if you want to know the color of the first pixel of the second line of character 'A', and store that value in variable C, you'll have to do: C:=Font^['A',1,2] Sorry about the confusing explanation... There is one routine that will be used in some of the example programs... It is a procedure that draws a column of a character in the right edge of the screen... All the scrollers in the examples scroll from right to left, because it's easier to read that way. So, here's the code for that routine: Procedure PutVertLine(C:Char;Index,Y:Byte;Where:Word); Var B:Byte; Begin For B:=1 To 16 Do PutPixel(319,Y+B,Font^[C,B,Index],Where); End; Y is the line in which the procedure starts to draw the character... Also, all the scrollers start in the middle of the screen... So let's do it... 5.2. Simple scroller The simple scroller is so simple I shouldn't waste time describing it... :) The ideia is just this: You write part of a character (with the PutVertLine procedure given above) and you scroll the screen to the left. Code: Program Simple_Scroller; Uses Mode13h,CFont,Crt; Var Colors:RgbList; Txt:String; Ch:Byte; A:Byte; Procedure PutVertLine(C:Char;Index,Y:Byte;Where:Word); Var B:Byte; Begin For B:=1 To 16 Do PutPixel(319,Y+B,Font^[C,B,Index],Where); End; Procedure ScrollLeft; Var B:Byte; Begin WaitVbl; For B:=90 To 110 Do Move(Mem[VGA:(320*B+1)],Mem[VGA:(320*B)],319); End; Begin { Initialization } InitGraph; LoadFont('Font.Fnt'); LoadPal('Font.Pal',Colors); SetPalette(Colors); Txt:='SPELLCASTER CODED THIS '; Txt:=Txt+'THIS SUCKS '; Txt:=Txt+'SO, SPELLCASTER SUCKS !!'; Txt:=Txt+' '; Ch:=1; { Scrolling } Repeat For A:=1 To 16 Do Begin PutVertLine(Txt[Ch],A,92,VGA); ScrollLeft; End; Inc(Ch); If Ch>Length(Txt) Then Ch:=1; Until Keypressed; { Shutting down } CloseGraph; End. This is simple... The ScrollLeft procedure was taught in the graphics section of issue 7 of 'The Mag'... The only difference is that this procedure only scrolls lines 90 through 110... It does that to speed up the program. This is a fairly quick program, so we didn't use any virtual screens. This should be easy to understand. We write do the screen the column indexed by variable A, then we increment it (the FOR cicle), then we scroll the screen to the left. We do this 16 times, and then we go on to the next character... When we finish with the string, we start all over again... 5.3. Sine scroller This is when the thing becomes tougher... What's a sine scroller ? It's a scroller than goes up and down, like a sine wave... If you don't know what a sine wave is, go to a 8th grade book... It should come there. There are lot's of ways to do this, and the one I'm thinking about is a little bit complicated. I'll draw a bit of ASCII: | |AAA BBBBBB CCCCCC D.... | AA BB BB CC CD DD .... | A B B C D D .... | AA AA BB CC DD DD .... | AAAAAA BBCCCC DDDDDD .... | ^ That is the left edge of the screen. The 'A' character represents a character's pixels, the 'B' character represents another character's pixels, etc... Only the top line of a character is shown, for simplicity stake... That is the first frame of movement... Let's see the second and third frame of movement: | |AAA BBBBBB CCCCCC E.... | AA BB BB CC DD DD .... | A B B C D D .... | AA AB BB CC DD DD .... | AAAAAA BCCCCC DDDDDD .... | | |AAA BBBBBB CCCCCD E.... | AA BB BB CC DD DE .... | A B B C D D .... | AA BB BB CC DD DD .... | AAAAAA CCCCCC DDDDDD .... | Notice that the absolute Y values are the same for every X in the screen, independent of the character that occupies that position... So, the ideia is to draw a part of the string in the screen, from X=0 to X=319, changing the Y according to a sine calculation, that is a function of X... This is harder than it looks. For example, in the first frame we have the 'plotting' of the string starting in character 'A' (presumably the first character of the string). Well, in the second frame, it also starts with character 'A'... So, what's the difference ? Well, in the second frame, the 'plotting' starts in the character's second column, and in the third frame, it starts on the third column, and so forth, until it arrives to the 16th column... There it switches to the first column of the next character in the string... The code looks awfully messy, but it works fine, and if you look carefully at it, it will be as clear as a day in the Carabeean... :) Notice that there is no need now for the ScrollLeft procedure, and that the PutVirtLine procedure was changed in order to enable the us to specify in which X coordinate to draw the column... Program Sine_Scroller; Uses Mode13h,CFont,Crt; Var Colors:RgbList; Txt:String; Ch,Ch1:Byte; A,Y,Index:Byte; X:Integer; Procedure PutVertLine(C:Char;Index,X,Y,Where:Word); Var B:Byte; Begin For B:=1 To 16 Do PutPixel(X,Y+B,Font^[C,B,Index],Where); End; Begin { Initialization } InitGraph; InitTables; LoadFont('Font.Fnt'); LoadPal('Font.Pal',Colors); SetPalette(Colors); Txt:='SPELLCASTER CODED THIS '; Txt:=Txt+'THIS SUCKS '; Txt:=Txt+'SO, SPELLCASTER SUCKS !!'; Txt:=Txt+' '; Ch:=1; { Scrolling } Repeat For A:=1 To 16 Do Begin Index:=A; X:=0; Ch1:=Ch; WaitVbl; Repeat Repeat Y:=100+Trunc(20*Sines^[X]); PutVertLine(Txt[Ch1],Index,X,Y,VGA); Inc(Index); Inc(X); Until (Index>16) Or (X>319); Index:=1; Inc(Ch1); If Ch1>Length(Txt) Then Ch1:=0; Until X>319; End; Inc(Ch); Until Keypressed; { Shutting down } CloseGraph; End. This looks bad, but after some days looking at it, it will be simple to understand... :) Any doubts, mail me... :) This could be speeded up a bit by using a precalculated table for the Y values... We are already using a pregenerated array for the sines. This could also be speeded up by using assembler... But I think you can manage that, if you really want... The next issue, I'll probably put more ASM code in the graphics section... 5.4. Mirror and water scroller Well, these two are fairly easy... Why ? Because they are effects upon the scrollers... So, you can have a sine-water scroller... Or a normal-water scroller. For the examples, I'll use the simple scroller as a basis... First, let's do the mirror scroller... That's the simpler one... The ideia is to flip what is scrolling around the X axis, that is, draw the image upside down... You could to this by drawing the scroller again, but this time with Y coordinates flipped... But that would be too slow to combine with a sine scroller... So, what I do is to grab the image that is already drawn and copy it below, but inverted... To do so, here's the DoMirror procedure: Procedure DoMirror; Var B:Byte; Begin WaitVbl; For B:=0 To 20 Do Move(Mem[VGA:(320*(90+B))],Mem[VGA:(320*(130-B))],320); End; It copies the 20 lines ranging from 90 to 110 (where the original scroller is) and puts it on the 20 lines ranging from 130 to 110 (respectivelly). You can addapt this procedure to mirror any effect... A nice touch is to dim a bit the inverted image, but I'll leave that to a future issue... Don't forget to call this procedure after you draw every frame. Here is a complete example: Program Mirror_Scroller; Uses Mode13h,CFont,Crt; Var Colors:RgbList; Txt:String; Ch:Byte; A:Byte; Procedure PutVertLine(C:Char;Index,Y:Byte;Where:Word); Var B:Byte; Begin For B:=1 To 16 Do PutPixel(319,Y+B,Font^[C,B,Index],Where); End; Procedure ScrollLeft; Var B:Byte; Begin WaitVbl; For B:=90 To 110 Do Move(Mem[VGA:(320*B+1)],Mem[VGA:(320*B)],319); End; Procedure DoMirror; Var B:Byte; Begin WaitVbl; For B:=0 To 20 Do Move(Mem[VGA:(320*(90+B))],Mem[VGA:(320*(130-B))],320); End; Begin { Initialization } InitGraph; LoadFont('Font.Fnt'); LoadPal('Font.Pal',Colors); SetPalette(Colors); Txt:='SPELLCASTER CODED THIS '; Txt:=Txt+'THIS SUCKS '; Txt:=Txt+'SO, SPELLCASTER SUCKS !!'; Txt:=Txt+' '; Ch:=1; { Scrolling } Repeat For A:=1 To 16 Do Begin PutVertLine(Txt[Ch],A,92,VGA); DoMirror; ScrollLeft; End; Inc(Ch); If Ch>Length(Txt) Then Ch:=1; Until Keypressed; { Shutting down } CloseGraph; End. So, here's for the mirror scroller... Now, let's move to the water scroller... This is a variation of the mirror scroller... The only diference is that the water scroller has some 'ripples' in the inverted mirror... How can we do that ? Simple... Instead of just copying the scroller image to other part of the screen, we shake it a bit, by using a random value... So, the DoMirror procedure is replaced by teh DoWater procedure: Procedure DoWater; Var B:Byte; Begin WaitVbl; For B:=0 To 20 Do Move(Mem[VGA:(320*(90+B))], Mem[VGA:(320*(130-B)+Random(3))],316); End; The rest remains the same... Look at the complete program: Program Water_Scroller; Uses Mode13h,CFont,Crt; Var Colors:RgbList; Txt:String; Ch:Byte; A:Byte; Procedure PutVertLine(C:Char;Index,Y:Byte;Where:Word); Var B:Byte; Begin For B:=1 To 16 Do PutPixel(319,Y+B,Font^[C,B,Index],Where); End; Procedure ScrollLeft; Var B:Byte; Begin WaitVbl; For B:=90 To 110 Do Move(Mem[VGA:(320*B+1)],Mem[VGA:(320*B)],319); End; Procedure DoWater; Var B:Byte; Begin WaitVbl; For B:=0 To 20 Do Move(Mem[VGA:(320*(90+B))], Mem[VGA:(320*(130-B)+Random(3))],316); End; Begin { Initialization } InitGraph; LoadFont('Font.Fnt'); LoadPal('Font.Pal',Colors); SetPalette(Colors); Txt:='SPELLCASTER CODED THIS '; Txt:=Txt+'THIS SUCKS '; Txt:=Txt+'SO, SPELLCASTER SUCKS !!'; Txt:=Txt+' '; Ch:=1; { Scrolling } Repeat For A:=1 To 16 Do Begin PutVertLine(Txt[Ch],A,92,VGA); DoWater; ScrollLeft; End; Inc(Ch); If Ch>Length(Txt) Then Ch:=1; Until Keypressed; { Shutting down } CloseGraph; End. Instead of using random values, you can use some sort of pregenerated table or sine wave to give a much smoother look... Ok, this article is finally finished... :) Scrollers are a pain in the ass in a lot of demos, but if you add some cool effects, you can make them cool things... So try to do a 3d-texture- -and-bump-mapped-phong-shaded-faster-than-light scroller... :))) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 6. First steps into virtual reality Welcome to the first article about 3d on 'The Mag'... Nowadays, everyone is excited with virtual reality and other stuff like that. Everytime I read the rec.games.programmer, I read tons of articles that say 'I wanna make a DOOM like game'... And in comp.sys.ibm.pc.demos, I read articles that say 'I wanna do a texture-mapped phong-shaded duck rotating in a voxel landscape !'... So, 3d is everywhere ! But let's move on... Let's think about the principle behind 3d. The ideia is to have a model of some kind and convert to an image. A model is a set of data that describe the virtual 'world'. As everyone knows, 3d stands for three-dimensions... But the computer screen only has 2 dimensions, so, every object in the computer can and is identified with 2 coordinates, X and Y, while everything in the real is described with 3 coordinates, X, Y and Z... It's fairly easy to estabilish the relation Z/Depth... But that's not always true... You can make Y your depth, or even an arbitrary axis... But it is convencioned that X is horizontal, Y is vertical and Z is the depth... So let's play with that... So, the main problem with 3d is to transform (x,y,z) points, in (x,y) points... So, how can we do that ? It's easier than you think... As you know the further away is an object, the closer to the horizon's center it is... So, the greater the Z coordinate, the closer it will be to the center of the screen. If we make all the coordinates relative to the center of the screen, the bigger the Z, the smaller will be X and Y... What's the operation that does this ? Division... So, given the (X,Y,Z) that describe a point in 3d space, the transformed coordinates (Xt,Yt) will be: Xt= X*256 Yt= Y*256 ----- ----- Z Z Why is that multiplication by 256 there ? Well, that's the camera factor. If we don't multiply, all would look pretty ugly, because the number would be too small... Why did I choose 256 ? For two reasons... First, that number looks good (if you use other numbers, you can even get a fish-eyed lens effect). Second, multiplication by 256 can be achieve by a lightning-fast shift-left operation, if we shift 8 bits... So, the procedure that converts 3 dimensions in 2 dimensions is the following: Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; We use reals for coordinates because of precision of rotations... There are problem with this routine if the Z is 0... But you don't need to compute points that have the Z equal to 0, because they are on top of the camera. Don't forget that the X,Y and Z are relative to the center of the virtual 'world', that is, relative to the camera. That 160 and 100 on the procedure are the center of the screen. When you plot a point in the screen, don't forget to check if the point lies inside the screen! I forgot that when I was coding an example program and it was giving me an headache... Here is a routine that plots a 3d point on the screen: Procedure Plot3d(X,Y,Z:Real;Color:Byte;Where:Word); Var Xt,Yt:Integer; Begin Xt:=160+Trunc(X*256/Z); If (Xt<0) Or (Xt>319) Then Exit; Xt:=100+Trunc(Y*256/Z); If (Yt<0) Or (Yt>199) Then Exit; Mem[Where:(320*Yt+Xt)]:=Color; End; Exit is a nifty command that makes Pascal exit the block it is in... If you do Exit inside a function or a procedure, the program goes back to where that function or procedure was called... If you do Exit in the main program, the program will end imediately. Why do we calculate the Xt and check if it is in the screen ? Because it is faster, if the point is outside the screen... If the point is outside the screen, there's no use for the Yt coordinate... Now that we've already drawn a 3d point, we can move on to the next section... 6.1. Wireframe graphics What are wireframe graphics ? Well, wireframe graphics are a kind of 3d graphics only made of points and lines connecting points, without any kind of shading, texturing or hidden-surface removal... So, to do wireframe graphics, we need a kind of structure that allows us to store the point's data and the information of what points are connect to each others... In our example, we'll use a static structure, but you could use a dinamic one... First, let's define the maximum number of points and lines an object can have: Const MaxPoints=30; MaxLines=30; Now, let's define the structures necessary: Type Point3d=Record X,Y,Z:Real; End; Object3d=Record NumberPoints:Byte; NumberLines:Byte; Pt:Array[1..MaxPoints] Of Point3d; Lines:Array[1..MaxLines,1..2] Of Byte; End; So, now we need to create an object: Var Car:Object3d; And afterwards, we need to load some data into the object... To do so, I made a small utility that enables you to type in 3d coordinates and to type which points connect, and store that data in a file. Then, you can read it back to Object3d variable with the LoadData procedure... The code for that procedure is below... The utility includes the source, so that you can change it anyway you see fit. The program is called 3DGEN.PAS. Notice that the program only uses integer numbers, integer's that are loaded into reals. Procedure Load3d(Filename:String;Var Obj:Object3d); Var F:Text; A:Byte; Begin Assign(F,Filename); Reset(F); ReadLn(F,Obj.NumberPoints); ReadLn(F,Obj.NumberLines); For A:=1 To Obj.NumberPoints Do ReadLn(F,Obj.Pt[A].X,Obj.Pt[A].Y,Obj.Pt[A].Z); For A:=1 To Obj.NumberLines Do ReadLn(F,Obj.Lines[A,1],Obj.Lines[A,2]); Close(F); End; Now, all we have to do is to join the points that are to be joined. Let's check out how this is done: Procedure Draw3d(Obj:Object3d;XOff,YOff,ZOff:Integer; Color:Byte;Where:Word); Var A:Byte; Pt1,Pt2:Byte; X1,Y1,X2,Y2:Integer; Begin For A:=1 To Obj.NumberLines Do Begin Pt1:=Obj.Lines[A,1]; Pt2:=Obj.Lines[A,2]; Conv3d(Obj.Pt[Pt1].X+XOff, Obj.Pt[Pt1].Y+YOff, Obj.Pt[Pt1].Z+ZOff, X1,Y1); Conv3d(Obj.Pt[Pt2].X+XOff, Obj.Pt[Pt2].Y+YOff, Obj.Pt[Pt2].Z+ZOff, X2,Y2); LineC(X1,Y1,X2,Y2,Color,Where); End; End; So, what's the deal here ? Well, simple... You have two lists in a structure. One has the coordinates of all the points in the object, and the other has the list the lines there are, stored by number of the point. For example, if Ln[1,1]:=1; Ln[1,2]:=5; That would mean that point 1 would connect to point 5... So, we traverse all the Lines array and connect the points to form the image... The XOff, YOff and ZOff variables are the offsets of the object... The object can be drawn in any region of 3d space. The LineC procedure that is called is a clipped version of the Line procedure I gave you in issue 4 of 'The Mag'. Check it's source in the Mode13h unit... The only diference between LineC and Line is that LineC checks if the point is within the borders of the screen. Let's see the complete example: Program WireFrame; Uses Mode13h,Crt; Const MaxPoints=30; MaxLines=30; Type Point3d=Record X,Y,Z:Real; End; Object3d=Record NumberPoints:Byte; NumberLines:Byte; Pt:Array[1..MaxPoints] Of Point3d; Lines:Array[1..MaxLines,1..2] Of Byte; End; Var A:Integer; Car:Object3d; D:Char; Procedure Conv3d(X,Y,Z:Real;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((X*256/Z)); Yt:=100+Trunc((Y*256/Z)); End; Procedure Load3d(Filename:String;Var Obj:Object3d); Var F:Text; A:Byte; Begin Assign(F,Filename); Reset(F); ReadLn(F,Obj.NumberPoints); ReadLn(F,Obj.NumberLines); For A:=1 To Obj.NumberPoints Do ReadLn(F,Obj.Pt[A].X,Obj.Pt[A].Y,Obj.Pt[A].Z); For A:=1 To Obj.NumberLines Do ReadLn(F,Obj.Lines[A,1],Obj.Lines[A,2]); Close(F); End; Procedure Draw3d(Obj:Object3d;XOff,YOff,ZOff:Integer; Color:Byte;Where:Word); Var A:Byte; Pt1,Pt2:Byte; X1,Y1,X2,Y2:Integer; Begin For A:=1 To Obj.NumberLines Do Begin Pt1:=Obj.Lines[A,1]; Pt2:=Obj.Lines[A,2]; Conv3d(Obj.Pt[Pt1].X+XOff, Obj.Pt[Pt1].Y+YOff, Obj.Pt[Pt1].Z+ZOff, X1,Y1); Conv3d(Obj.Pt[Pt2].X+XOff, Obj.Pt[Pt2].Y+YOff, Obj.Pt[Pt2].Z+ZOff, X2,Y2); LineC(X1,Y1,X2,Y2,Color,Where); End; End; Begin Initgraph; Load3d('Car.3d',Car); SetColor(1,63,63,0); For A:=-300 To 300 Do Begin Draw3d(Car,-20,0,A,1,VGA); Draw3d(Car,-20,0,A,0,VGA); End; D:=Readkey; Closegraph; End. 6.2. Basic transformations Everything in 3d should be done with matrixes, since they ease up a lot of calculations... I will explain latter why is this true. Matrixes are a very important part of algebra. So, I'll start this section by explaining basic matrix calculus. Imagine an 3x3 array and a 1x3 array: Ú ¿ Ú ¿ ³1 2 3³ ³1³ A= ³4 5 6³ B= ³2³ ³7 8 9³ ³3³ À Ù À Ù Matrixes are identified by a uppercase letter. B is also called a vector, and because it has 3 numbers, it is called a three-dimensional vector. Notice that you can think B as three coordinates to a point in 3d: X=1, Y=2 and Z=3. With this in mind, let's see matrix multiplication. Let's multiply A by B: Ú ¿ Ú ¿ Ú ¿ Ú ¿ ³1 2 3³ ³1³ ³1*1+2*2+3*3³ ³14³ A*B= ³4 5 6³*³2³=³4*1+5*2+6*3³=³32³ ³7 8 9³ ³3³ ³7*1+8*2+9*3³ ³50³ À Ù À Ù À Ù À Ù This is an example, only... The rule is: Ú ¿ Ú ¿ Ú ¿ ³a b c³ ³x³ ³ax+by+cz³ A*B= ³d e f³*³y³=³dx+ey+fz³ ³g h i³ ³z³ ³gx+hy+iz³ À Ù À Ù À Ù You can multiply matrixes of any size, but we only need to know how to multiply a 3x3 matrix by a 1x3 matrix and a 4x4 matrix by a 1x4 matrix. There's just one rule to matrix multiplication: The number of columns of the first matrix must be equal to the number of lines of the second matrix. Here's the rule for 4x4 by 1x4 multiplication: Ú ¿ Ú ¿ Ú ¿ ³a b c d³ ³v³ ³ax+by+cz+dv³ A*B= ³e f g h³*³x³=³ex+fy+gz+hv³ ³i j k l³ ³y³ ³ix+jy+kz+lv³ ³m n o p³ ³z³ ³mx+ny+oz+pv³ À Ù À Ù À Ù Other thing you should know is the identity matrix: 3x3 identity matrix: Ú ¿ 4x4 identity matrix: Ú ¿ ³1 0 0³ ³1 0 0 0³ ³0 1 0³ ³0 1 0 0³ ³0 0 1³ ³0 0 1 0³ À Ù ³0 0 0 1³ À Ù Plainly put, the identity matrix is a matrix with all the elements zero, except the elements in the main diagonal. The identity matrix has the property of not afecting the multiplication. So: Ú ¿ Ú ¿ Ú ¿ Ú ¿ ³1 0 0³ ³5³ ³1*5+0*7+0*8³ ³5³ ³0 1 0³*³7³=³0*5+1*7+0*8³=³7³ ³0 0 1³ ³8³ ³0*5+0*7+1*8³ ³8³ À Ù À Ù À Ù À Ù Other thing that you need to know is multiplying two 3x3 matrixes and two 4x4 matrix: Ú ¿ Ú ¿ Ú ¿ ³a b c³ ³j k l³ ³aj+bm+cp ak+bn+cq al+bo+cr³ ³d e f³*³m n o³=³dj+em+fp dk+en+fq dl+eo+fr³ ³g h i³ ³p q r³ ³gj+hm+ip gk+hn+iq gl+ho+ir³ À Ù À Ù À Ù Multiplying two 4x4 matrixes is similar. The rules for multiplying matrixes of any size are the following: - The number of columns of the first matrix must be equal to the number of lines of the second matrix. Example: You want to multiply two matrixes, with dimensions AxB and CxD (where A and C are the number of columns). You can only multiply if A and D are equal. - The result matrix has the same number of columns of the second matrix and the same number of lines as the number of columns of the first matrix. Example: The result matrix of multiplying the above matrixes would have C columns and A lines. - The item (item is one of the factors of the matrix) in position (x,y) of the matrix will be equal to the multiplication of line number Y with columns number X. - Matrix multiplication is NOT comutative, that is, you can not change the order of the operands ! This is the basic stuff on matrixes... If you want to know more, go to any good algebra book... If I forgot something, I'll tell you in the explanation. Keep this in mind, because we'll use this later... Now, let's move on to the theory of the diferent transformations... Let's start on translation... Translation is the easiest operation of them all. Translating an object means moving it from it's current position to other position. To translate an object, you just have to add to it's coordinates a number. So, let's do a translation procedure: Procedure Translate(Var Obj:Object3d;XOff,YOff,ZOff:Integer); Var A:Byte; Begin For A:=1 To Obj.NumberPoints Do Begin Obj.Pt[A].X:=Obj.Pt[A].X+XOff; Obj.Pt[A].Y:=Obj.Pt[A].Y+YOff; Obj.Pt[A].Z:=Obj.Pt[A].Z+ZOff; End; End; Easy, isn't it ? Now, let's do something you'll think it's pretty useless, but that can have it's uses... Let's put the translation in a matrix form: The formula used for translation is as follows: X = Xo+XOff Y = Yo+YOff Z = Zo+ZOff Where X, Y and Z are the translated coordinates; Xo, Yo and Zo are the original coordinates and XOff, YOff and ZOff are the offsets of the coordinates. Putting this in matrix form: Ú ¿ Ú ¿Ú ¿ Ú ¿ ³ X ³ ³ 1 0 0 0 ³³Xo³ ³Xo+XOff³ P = ³ Y ³ = ³ 0 1 0 0 ³³Yo³ = ³Yo+YOff³ ³ Z ³ ³ 0 0 1 0 ³³Zo³ ³Zo+ZOff³ ³ 1 ³ ³XOff YOff ZOff 1 ³³ 1³ ³ 1 ³ À Ù À ÙÀ Ù À Ù We'll see later the use of this... Don't forget you can discard that last one in the result column... :) So, let's move on to other basic transformation: Scaling ! Let's see a 2d example... The 3d theory is equal. Scaling is the operation that transforms this: ³ ³ ³ ³ ³ ³ +----+ ³ +--+ in this: ³ | | ³ | | ³ | | ³ +--+ ³ +----+ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ This is a scaling by two... To do so, we just have to multiply every point by two, right ? WRONG! If you multiply each point by two, all you'll get is a translation: ³ ³ ³ ³ ³ ³ +--+ ³ +--+ *2 = ³ | | ³ | | ³ +--+ ³ +--+ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ So, what do we need to do ? Check out this case: ³ ³ ³ ³ ³ +---+ +-+ | ³ | ÄÄÄÄÄÄÄÄ|Å|ÄÄÄÄÄÄÄÄ *2= ÄÄÄÄÄÄÄ|ÄÅÄ|ÄÄÄÄÄÄÄÄ +-+ | ³ | ³ +---+ ³ ³ ³ ³ Have you figured out what you have to do already ? You have to move the object to the center of the world (point [0,0,0]), to be able to scale it properly... Procedure to scale: Procedure Scale(Var Obj:Object3d;XScl,YScl,ZScl:Real); Var A:Byte; Begin For A:=1 To Obj.NumberPoints Do Begin Obj.Pt[A].X:=Trunc(Obj.Pt[A].X*XScl); Obj.Pt[A].Y:=Trunc(Obj.Pt[A].Y*YScl); Obj.Pt[A].Z:=Trunc(Obj.Pt[A].Z*ZScl); End; End; This procedure can have other uses... You can shrink an image, by specifying a scaling factor smaller than 1. If you specify a negative number, the object will be inverted. If you don't want to scale a certain axis, specify 1, not 0, because 0 would nulify that coordinates... Now, let's see scaling in a matrix form: X = Xo*XScl Y = Yo*YScl Z = Zo*ZScl Where X, Y and Z are the scaled coordinates; Xo, Yo and Zo are the original coordinates and XScl, YScl and ZScl are the scaling factors... Putting this in matrix form: Ú ¿ Ú ¿Ú ¿ Ú ¿ ³ X ³ ³ XScl 0 0 ³³Xo³ ³Xo*XScl³ P = ³ Y ³ = ³ 0 YScl 0 ³³Yo³ = ³Yo*YScl³ ³ Z ³ ³ 0 0 ZScl ³³Zo³ ³Zo*ZScl³ À Ù À ÙÀ Ù À Ù See ? Easy... :) Now we can see the use for matrixes. Imagine that some transformation involved translation AND scaling. You can do both in just one step... All you have to do is multiply the matrixes that do these transformations. So, matrix for this operation is: Ú ¿ Ú ¿ Ú ¿ ³ 1 0 0 0 ³ ³ XScl 0 0 0 ³ ³ XScl 0 0 0 ³ ³ 0 1 0 0 ³ ³ 0 YScl 0 0 ³ ³ 0 YScl 0 0 ³ ³ 0 0 1 0 ³*³ 0 0 ZScl 0 ³=³ 0 0 ZScl 0 ³ ³XOff YOff ZOff 1 ³ ³ 0 0 0 1 ³ ³ XOff*XScl YOff*YScl ZOff*ZScl 1 ³ À Ù À Ù À Ù Applying a vector: Ú ¿Ú ¿ Ú ¿ ³ XScl 0 0 0 ³³Xo³ ³ Xo*XScl ³ ³ 0 YScl 0 0 ³³Yo³ ³ Yo*YScl ³ ³ 0 0 ZScl 0 ³³Zo³=³ Zo*ZScl ³ ³ XOff*XScl YOff*YScl ZOff*ZScl 1 ³³1 ³ ³Zo*XOff*ZScl+Yo*YOff*ZScl+Zo*ZOff*ZScl³ À ÙÀ Ù À Ù In equation formula: X=(Xo+XOff)*XScl; Y=(Yo+YOff)*YScl; Z=(Zo+ZOff)*ZScl; This is what you expected to found... In this case, matrix multiplication isn't very useful, but in cases with lots of transformations, including rotations and other things like that, this becomes very useful ! Now, let's move on to the other type of transformation, and probably one of the most importants... It can be even more important than translation. But it is also harder... Yep, I'm talking about rotations... We'll first analise the case of planar rotation, that is, a rotation that is in order to a plane, or (if you prefer) an axis... I'll explain later how to generalize this to 3d rotation... So, in a rotation, you want to transform point (x,y) in a point (x',y'). Below is an example of a 45 degrees rotation: Y Y ³ ³ ³ O (x',y') ³ O (x,y) | ³ / --> | ³ / | ³/alpha |beta ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄX ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄX Alpha is the angle the point makes with the X axis... In this case, it is 45 degrees. Beta is the angle the transformed point makes with the X axis, in this case, 90 degrees. Notice that if this was a 3d rotation, it would be a rotation around the Z axis... That means that the Z coordinate of the points is never changed. Let's do some formulas to get to the final one... We know (or if you don't know, get an 8th grade book and read the trigono- metry part) that a point can be expressed by an (X,Y) ordinated pair or a (P,A) pair, in which p is the distance of the point to the axis and a is the angle the point makes with one of the other axis. We also know the relations between the two systems of coordinates: X = P*Cos(A) Y = P*Sin(A) P = Sqrt(X^2+Y^2) A = ArcCos(X/P) = ArcSin(Y/P) Sqrt is the square root... The (X,Y) coordinate system is called the carthesian system, and the (P,A) system is called the polar coordinates system. Let's imagine we have point A, with carthesian coordinates (X,Y). That would convert in polar coordinates (P1,A1), like this: P1 = Sqrt(X^2+Y^2) A1 = ArcCos(X/P1) = ArcSin(Y/P1) and we want to rotate the point by an angle B. That would get us point C, with polar coordinates (P2,A2). As we can see from the above example (and from simple maths), P1 is equal to P2. Now, we want to obtain the carthesian coordinates of point C. So, from the above formulas we know that the carthesian coordinates of point C are: X' = P1*Cos(A2) Y' = P1*Sin(A2) But, we also know that A2=A1+B, because B is the angle we rotated and A1 is the original angle. So: X' = P1*Cos(A1+B) Y' = P1*Sin(A1+B) Again, with a simple knowledge of trigonometry, we know that: Sin(U+V) = Sin(U)*Cos(V) + Cos(U)*Sin(V) Cos(U+V) = Cos(U)*Cos(V) - Sin(U)*Sin(V) So, applying to the formula above: X' = P1*[Cos(A1)*Cos(B)-Sin(A1)*Sin(B)] Y' = P1*[Sin(A1)*Cos(B)+Cos(A1)*Sin(B)] From above, we know that: A1 = ArcCos(X/P1) = ArcSin(Y/P1) And from that, we derive that: Cos(A1) = X/P1 Sin(A1) = Y/P1 So, applying in the other formula: X' = P1*[(X/P1)*Cos(B)-(Y/P1)*Sin(B)] Y' = P1*[(Y/P1)*Cos(B)+(X/P1)*Sin(B)] And again, applying simple maths, we get the final formula: X' = X*Cos(B) - Y*Sin(B) Y' = Y*Cos(B) + X*Sin(B) See ? It is fairly easy to calculate... Now, let's do a simple procedure: Procedure RotateZ(Var Obj:Object3d;Deg:Integer); Var A:Byte; Angle:Real; XTemp:Real; Begin Angle:=0.0175*Deg; For A:=1 To Obj.NumberPoints Do With Obj.Pt[A] Do Begin XTemp:=X; X:=XTemp*Cos(Angle)-Y*Sin(Angle); Y:=Y*Cos(Angle)+XTemp*Sin(Angle); End; End; Angle is the equivalent to Deg, but expressed in radians. We need that because the Sin and Cos functions use radians. We must use a temporary variable, because the X value is altered in the first expression, but we need X's old value for the second expression... As usual, let's move this to matrix form... We need a 2x2 matrix: X' = X*Cos(Ang) + Y*Sin(Ang) Y' = Y*Cos(Ang) - X*Sin(Ang) The matrix corresponding to these formulas is: Ú ¿ ³ Cos(Ang) -Sin(Ang) ³ ³ Sin(Ang) Cos(Ang) ³ À Ù Adapting this to a 3d rotation, we'll get: Ú ¿ ³ Cos(Ang) -Sin(Ang) 0 ³ ³ Sin(Ang) Cos(Ang) 0 ³ ³ 0 0 1 ³ À Ù Remember what I said about the Z coordinate being unaltered by the rotation ? That's why the Z line isn't altered (the Z column is the line), and Z doesn't enter any of the calculations (the Z collumn isn't altered). Now, if you change the name of the axis, you can get the formulas for the rotation around the other axis... The formulas, matrixes and procedures that achieve these effects follow... Around the X axis: Z' = Z*Cos(Ang) + Y*Sin(Ang) Y' = Y*Cos(Ang) - Z*Sin(Ang) Ú ¿ ³ 1 0 0 ³ ³ 0 Cos(Ang) -Sin(Ang) ³ ³ 0 Sin(Ang) Cos(Ang) ³ À Ù Procedure RotateX(Var Obj:Object3d;Deg:Integer); Var A:Byte; Angle:Real; ZTemp:Real; Begin Angle:=0.0175*Deg; For A:=1 To Obj.NumberPoints Do With Obj.Pt[A] Do Begin ZTemp:=Z; Z:=ZTemp*Cos(Angle)-Y*Sin(Angle); Y:=Y*Cos(Angle)+ZTemp*Sin(Angle); End; End; Around the Y axis: X' = X*Cos(Ang) + Z*Sin(Ang) Z' = Z*Cos(Ang) - X*Sin(Ang) Ú ¿ ³ Cos(Ang) 0 -Sin(Ang) ³ ³ 0 1 0 ³ ³ Sin(Ang) 0 Cos(Ang) ³ À Ù Procedure RotateY(Var Obj:Object3d;Deg:Integer); Var A:Byte; Angle:Real; XTemp:Real; Begin Angle:=0.0175*Deg; For A:=1 To Obj.NumberPoints Do With Obj.Pt[A] Do Begin XTemp:=X; X:=XTemp*Cos(Angle)-Z*Sin(Angle); Z:=Z*Cos(Angle)+XTemp*Sin(Angle); End; End; You can see in the file 3D1.PAS an example program of all the rotations being made, one at a time. Notice that we have to move the objects to the origin of the world, in order for the rotation work correctly... That's because the origin (0,0,0) is the center of rotation... If you want another point to be the origin, you have to calculate the coordinates of the points of the object relative to that point, rotate those relative coordinates, and then calculate those coordinates relatively to the origin of the world. That's easier to do than it sounds... Just some subtractions and addictions... Now, imagine you wanted to rotate the object in the three axis... You just rotated around the various axis, using the formulas... To do so, you could use the following procedure: Procedure Rotate(Var Obj:Object3d;XRot,YRot,ZRot:Integer); Begin RotateX(Obj,XRot); RotateY(Obj,XRot); RotateZ(Obj,XRot); End; In 3D2.PAS you can see an example of this... Another way you can do the three rotations is by joining the three matrixes of rotation together... Like this: Ú ¿ Ú ¿ Ú ¿ ³ Cos(a) -Sin(a) 0 ³ ³ Cos(a) 0 -Sin(a) ³ ³ 1 0 0 ³ ³ Sin(a) Cos(a) 0 ³*³ 0 1 0 ³*³ 0 Cos(a) -Sin(a) ³ ³ 0 0 1 ³ ³ Sin(a) 0 Cos(a) ³ ³ 0 Sin(a) Cos(a) ³ À Ù À Ù À Ù I'm not in the mood for arithmetic now... So, do the calculations yourself... It's a good exercice... To speed up the routines a bit, you can consider precalculating the values of the Sin(a) and Cos(a), because they will be very used in the routine. Other thing you can do to speed up the calculations is to use a pregenerated sine/cosine table... See 3D3.PAS to see an example... Now, let's move on to another subject... 6.3. A 3D starfield The 3d starfield is one of the more used effects in yesterdays demos... As the matter of fact, there are lot's of demos that include starfields nowadays, with some new twist... Well, let's move on... What's a 3d starfield ? A 3d starfield is a starfield similar to the one I showed you in a previous issue of 'The Mag', but instead of just moving sideways, in the 3d starfield you are moving into it... You see the stars passing by... This is so simple to do... You just have an array of points, and you move them (using a translation), while you can rotate it around the Z axis, in order to get a cooler effect... Other thing you can do is to change the color of the point... The point can be darker if it is further away from the viewer and it gets brighter as it comes nearer. Other 3d starfields move around inside the starfield, but that's simple to do... It's just movement... This is so easy to make, that I'm gonna just splat the code here... Look at it and it will be clear... Program Starfield; Uses Mode13h,Crt; Const Points=200; Speed=15; RotateSpeed=1; Type Point3d=Record X,Y,Z:Real; End; Var Stars:Array[1..Points] of Point3d; A:Integer; X,Y:Integer; Color:Byte; D:Char; Procedure Conv3d(P:Point3d;Var Xt,Yt:Integer); Begin Xt:=160+Trunc((P.X*256)/P.Z); Yt:=100+Trunc((P.Y*256)/P.Z); End; Procedure RotateZ(Deg:Integer); Var Angle:Real; XTemp:Real; S,C:Real; Begin Angle:=0.0175*Deg; S:=Sin(Angle); C:=Cos(Angle); For A:=1 To Points Do Begin XTemp:=Stars[A].X; Stars[A].X:=XTemp*C-Stars[A].Y*S; Stars[A].Y:=Stars[A].Y*C+XTemp*S; End; End; Begin { Setup graphics } InitGraph; InitVirt; Cls(0,VGA); Cls(0,VP[1]); { Setup grayscale } For A:=0 To 15 Do SetColor(A,A*4,A*4,A*4); { Setup stars } For A:=1 To Points Do Begin Stars[A].X:=Random(640)-320.0; Stars[A].Y:=Random(400)-200.0; Stars[A].Z:=Random(800); End; { Main cicle } Repeat { Move stars } For A:=1 To Points Do Stars[A].Z:=Stars[A].Z-Speed; { Rotate stars } RotateZ(RotateSpeed); { Check if any stars are very near of the viewer... If they are, reset them... } For A:=1 To Points Do If Stars[A].Z<=100 Then Stars[A].Z:=800; { Clear virtual screen } Cls(0,VP[1]); { Draw stars in virtual screen } For A:=1 To Points Do Begin Conv3d(Stars[A],X,Y); { Determine color } Color:=15-(Trunc((16*Stars[A].Z)) Div 1000); { The following procedure is equal to the normal putpixel, except that this check if the point is in the boundaries of the screen } PutClippedPixel(X,Y,Color,VP[1]); End; { Copy virtual screen to VGA screen } CopyPage(VP[1],VGA); Until Keypressed; { Shutdown } CloseVirt; Closegraph; End. This is easy to understand... All the concepts were given back in this article... Instead of drawing lines, you just put points... This could be speeded up by using assembler... It is so easy to code a starfield in ASM ! But let's move on to another cool thing... 6.4. VectorBalls What are vector balls ? Well, I could explain what are vector balls, but it's easier to show. Execute program VECTOR1.PAS and you'll see... It's a cool effect... It's name comes from the fact that they are balls, and that they use vectors... What is a vector ? A vector is a set of coordinates (in this case, three) that define a point in space or a direction... This is not a 'by the book' explanation, but it will have to do. Vectorballs have (x,y,z) coordinates that represent they're position in space. All you have to do is convert it's 3d coordinates to 2d coordinates and draw the ball... We use 2 diferent colors for the balls just to achieve an cooler effect... The only catch of vectorballs is that you must sort the vectorballs before drawing, so that the balls that are further away are draw first, so that those that are in front overwrite. If you don't do this, the effect wouldn't look like 3d at all... Try removing the call to the Sort procedure in the DrawBalls procedure... We used QuickSort, because it's the faster method, and speed is important in this effect. This is a fairly easy to do program, and it is all comented, so you shouldn't have dificulty in understanding it... If you wanted to do objects with more than two colors, it is better to use a base sprite and add the number of the color. If you don't understand what I'm talking about, check out the VECTOR2.PAS program... Check the DrawSprite routine in that one. That program also uses a procedure called LoadVector, that reads for disk a file containing the data for the color and base coordinates of the vectorballs. That data is generated by the VECTGEN.PAS program. You can also add movement, by using the Move procedure... Movement in the procedure is cool... This is the end of the first article on 3d in 'The Mag'... I don't know when I'll do the next part, but it will be about poligons and sorting... And maybe a little bit of lighting... But try to figure this out for yourself... :) -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 7. Sprites Part III - Lots of fun stuff Hello again... Let's get going with sprites in this article... This is gonna be a quick one, because I want to wrap this issue of 'The Mag' quickly, 'cos this is more than 120Kb already... 7.1. Transparency Have you noticed that when you put down a sprite, someparts of it obscures what it was there before ? Well, to solve that, we use transparency... I already talked about transparency in the Fonts article I did last issue... I will just go over the algorithm... The ideia is this: if the color of the pixel to place is 0, then don't put down that pixel, in the other case, put it down. Simple and easy... You aren't limited to color 0... It can be any color... 0 just became the standart... Procedure talking: Procedure PutImage_T(X,Y:Word;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; For A:=Y To Y+Dy-1 Do Begin For B:=X To X+Dx-1 Do Begin If Mem[Segm:Offs]<>0 Then PutPixel(B,A,Mem[Segm:Offs],Where); Inc(Offs); End; End; End; Procedure PutImage_CT(X,Y:Integer;Var Img:Pointer;Where:Word); Var Dx,Dy:Word; A,B:Word; Segm,Offs:Word; Begin Segm:=Seg(Img^); Offs:=Ofs(Img^); Move(Mem[Segm:Offs],Dx,2); Move(Mem[Segm:Offs+2],Dy,2); Offs:=Offs+4; A:=Y; While (A<=Y+DY-1) And (A=MinX) And (Y>=MinY) And (Mem[Segm:Offs]<>0) Then PutPixel(B,A,Mem[Segm:Offs],Where); Inc(Offs); Inc(B); End; Inc(A); End; End; There are two procedures, as you may already founded out... PutImage_T is for putting images using transparency and no-clipping. PutImage_CT is identically, but it performs clipping. The disadvantages of using transparency is that the procedures become slower. In order of speed: PutImage - No clipping / No transparency PutImage_T - No clipping / Transparency PutImage_C - Clipping / No transparency PutImage_CT - Clipping / Transparency You should choose carefully the type of PutImage you use, for the sake of speed... For example, to put down tiles, use PutImage, because you don't need transparency... Just use your head, before using the procedures. Just a note... We shouldn't call this transparency, but masking... Transparency is another thing, I will cover that in a future issue... 7.2. Moving over a background In almost every game sprites are required... Usually they move above a background of some sort (take for example the mouse pointer... That can be thought as a sprite). Wouldn't it be great that the sprite could be moved without destroying the background ? Of course it would... In some computers (like the AMIGA), there is a chip that does this for us... In the PC, we'll have to do it ourselfs... This is easy... All you have to do is to grab a piece of image of the same size as the sprite, in the same location you will put the sprite down. Then, you put the sprite down... Every time you erase the sprite, instead of blacking the zone were the sprite was, you put the image you previously grabbed, so that everything remains the same. Then, you start all over again... Let's check some code: Program SpritesOverBackground; Uses Crt,Sprites,Mode13h; Type Sprite=Record Img:Pointer; Back:Pointer; X,Y:Integer; End; Var F:File; Ship:Sprite; C:Char; Begin { Load sprite image } Assign(F,'Ship.Img'); Reset(F,1); LoadImage(F,Ship.Img); Close(F); { Init graphics and set palette } InitGraph; InitVirt; SetColor(1,63,0,0); SetColor(2,63,40,0); SetColor(3,63,63,0); { Load a background } LoadPCX('Planet.Pcx',VP[1]); SetPalette(PCXPal); { Init ship } Ship.X:=0; Ship.Y:=100; { Move the ship } Repeat { Get a 27x10 square of the place were the ship is } GetImage(Ship.X,Ship.Y,Ship.X+27,Ship.Y+10, Ship.Back,VP[1]); { Put sprite of ship, using masking } PutImage_T(Ship.X,Ship.Y,Ship.Img,VP[1]); { Copy virtual screen to VGA screen } CopyPage(VP[1],VGA); { Restore background } PutImage(Ship.X,Ship.Y,Ship.Back,VP[1]); KillImage(Ship.Back); { Move ship } Ship.X:=Ship.X+2; Until (Ship.X>292) Or (KeyPressed); C:=ReadKey; KillImage(Ship.Img); CloseVirt; CloseGraph; End. Notice the use of virtual screens... Almost everything that involves sprites must use virtual screens, in order to smooth the movements... Try not using virtual screens in the above program and see the crappy result... 7.3. Flipping a sprite Wouldn't it be cool if the ship in the end would get back and forth ? Of course it would... But that would require two images, one pointing right and the other pointing left... Unless you use flipping... Flipping is a routine that inverts an image, like a mirror... This is easy to do, just think a bit... The first column would be the last column, the second column would be the 2nd last column, etc... This is horizontal flipping... For vertical flipping, the 1st line would become the last line, etc... Coding the horizontal flipping is easy: Procedure FlipHoriz(Var Img:Pointer); Var Dx,Dy:Word; S1,O1:Word; S2,O2:Word; Tmp:Pointer; A,B:Word; Begin { Get X and Y sizes } S1:=Seg(Img^); O1:=Ofs(Img^); Move(Mem[S1:O1],Dx,2); Move(Mem[S1:O1+2],Dy,2); { Create temporary sprite } GetMem(Tmp,Dx*Dy+4); S2:=Seg(Tmp^); O2:=Ofs(Tmp^); { Put the size of the sprite in the temporary sprite } Move(Mem[S1:O1],Mem[S2:O2],4); { Move the columns } For A:=0 To Dx-1 Do For B:=0 To Dy-1 Do Move(Mem[S1:O1+(B*Dx+A+4)], Mem[S2:O2+(B*Dx+(Dx-A-1)+4)],1); { Kill old image } KillImage(Img); { Copy new image to old one } Img:=Tmp; End; The vertical flipping is even easier: Procedure FlipVert(Var Img:Pointer); Var Dx,Dy:Word; S1,O1:Word; S2,O2:Word; Tmp:Pointer; A:Word; Begin { Get X and Y sizes } S1:=Seg(Img^); O1:=Ofs(Img^); Move(Mem[S1:O1],Dx,2); Move(Mem[S1:O1+2],Dy,2); { Create temporary sprite } GetMem(Tmp,Dx*Dy+4); S2:=Seg(Tmp^); O2:=Ofs(Tmp^); { Put the size of the sprite in the temporary sprite } Move(Mem[S1:O1],Mem[S2:O2],4); { Move the lines } For A:=0 To Dy-1 Do Move(Mem[S1:O1+(A*Dx+4)], Mem[S2:O2+((Dy-1-A)*Dx+4)],Dx); { Kill old image } KillImage(Img); { Copy new image to old one } Img:=Tmp; End; All these routines are included in the Sprites unit... You can see a test program that makes the ship go right and left above a background in the file FLYSHIP.PAS. Cya in the next article... -x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x- 8. Graphics Part IX - Polygons Polygons is a comes from a greek word that means 'many angles'. Speaking in general terms, a polygon is a planar set of points joined by lines that is closed. By closed, I mean that the last point joins with the first point. Circles and ellipses are special polygons, because they have an infinite number of points. Arcs are just here because I want to explain them. It's just a derivation of ellipses. For now forth, when I talk about polygons, I'm talking about 4 sided polygons, but that concept can be extended to any number of sides. For three sided polygons (triangles) this gets easier, and usually more than four sides aren't used. 8.1. Simple polygons Four sided polygons are constructed of 4 points, each one of them having a (x,y) coordinate. Point 1 is joined with a line to point 2, point 2 to point 3, point 3 to point 4 and finally, point 4 to point 1. This is easy to do: Procedure Poly(X1,Y1,X2,Y2,X3,Y3,X4,Y4:Word;Color:Byte;Where:Word); Begin Line(X1,Y1,X2,Y2,Color,Where); Line(X2,Y2,X3,Y3,Color,Where); Line(X3,Y3,X4,Y4,Color,Where); Line(X4,Y4,X1,Y1,Color,Where); End; See how easy it was ? Now, let's move on to something more complex... 8.2. Filled polygons The ideia behind filled polygons is fairly simple. You just draw horizontal lines between the edges of the polygon. Like this: 2 Check out the horizontal lines. /\ The hardest thing in this way of filling polygons /--\3 (because they are several different ways of doing /---/ this) is to determine the minimum and maximum 1\--/ X coordinates, which we use to draw the horizontal \/ line. 4 In the 8th grade (and in a previous issue) you probably learned that any line can be defined by the equation: (y-y0)=m(x-x0) As we want to know the X coordinate, let's solve the equation in order to X: (y-y0) x= ------ + x0 m As you probably know, the m is the tangent of the line, and it is defined as: x1-x0 m= ----- y1-y0 So, the final formula is: (y-y0)*(x1-x0) x= -------------- + x0 (y1-y0) So, for any line in the polygon (in the above example, from the Y coordinate of point 2 to the Y coordinate of point 4), you must determine which is the line from which you will derive the X coordinate, to see if it is the maximum and minimum. To know which is the line, you simply compare see if the Y coordinate you are checking is between the Y coordinates of the various points. For example, imagine that the points in the example above had the following coordinates: 1: X1=5 ; Y1=25 2: X2=20 ; Y2=10 3: X3=30 ; Y3=20 4: X4=15 ; Y4=35 And you wanted to know the maximmum and minimum X for line 17. Comparing the Y values, you know that line 17 will intersect the line between points 1 and 2, and the line between points 2 and 3. So, using the above coordinate, you know that the X coordinates of line 17 in those lines will be respectively 13 and 27. Now, you just had to draw a horizontal line between (13,17) to (27,17). The procedure to do the polygon follows: Procedure FPoly(X1,Y1,X2,Y2,X3,Y3,X4,Y4:Word;Color:Byte;Where:Word); Var MnY,MxY:Word; DeltaX1,DeltaX2,DeltaX3,DeltaX4:Integer; DeltaY1,DeltaY2,DeltaY3,DeltaY4:Integer; Y:Word; MnX,MxX:Integer; X:Integer; Begin { Find out the lines that are to be scanned } MnY:=Y1; MxY:=Y1; If MnY>Y2 Then MnY:=Y2; If MnY>Y3 Then MnY:=Y3; If MnY>Y4 Then MnY:=Y4; If MxY199 Then MxY:=199; If MnY>199 Then Exit; If MxY<0 Then Exit; { Precalculate the (x1-x0) and (y1-y0) needed later, because they remain the same for all the routine. They are needed to calculate the M. We don't calculate the M, because that would involve real maths that is slower than calculating in an integer form later in the code } DeltaX1:=(X1-X4); DeltaY1:=(Y1-Y4); DeltaX2:=(X2-X1); DeltaY2:=(Y2-Y1); DeltaX3:=(X3-X2); DeltaY3:=(Y3-Y2); DeltaX4:=(X4-X3); DeltaY4:=(Y4-Y3); { Main loop } For Y:=MnY To MnX Do Begin { Find out the minummum and maximmum X coord } MnX:=319; MxX:=-1; { Check if the line intersects line (X1,Y1)->(X2,Y2) } If (Y>=Y1) Or (Y>=Y2) Then If (Y<=Y1) Or (Y<=Y2) Then { Insure that the points don't have the same Y coord } If Not(Y1=Y2) Then Begin { Point of intersection } X:=(Y-Y1)*DeltaX2 Div DeltaY2 + X1; If XMxX Then MxX:=X; End; { Check if the line intersects line (X2,Y2)->(X3,Y3) } If (Y>=Y2) Or (Y>=Y3) Then If (Y<=Y2) Or (Y<=Y3) Then { Insure that the points don't have the same Y coord } If Not(Y2=Y3) Then Begin { Point of intersection } X:=(Y-Y2)*DeltaX3 Div DeltaY3 + X2; If XMxX Then MxX:=X; End; { Check if the line intersects line (X3,Y3)->(X4,Y4) } If (Y>=Y3) Or (Y>=Y4) Then If (Y<=Y3) Or (Y<=Y4) Then { Insure that the points don't have the same Y coord } If Not(Y3=Y4) Then Begin { Point of intersection } X:=(Y-Y3)*DeltaX4 Div DeltaY4 + X3; If XMxX Then MxX:=X; End; { Check if the line intersects line (X4,Y4)->(X1,Y1) } If (Y>=Y4) Or (Y>=Y1) Then If (Y<=Y4) Or (Y<=Y1) Then { Insure that the points don't have the same Y coord } If Not(Y4=Y1) Then Begin { Point of intersection } X:=(Y-Y4)*DeltaX1 Div DeltaY1 + X4; If XMxX Then MxX:=X; End; { Horizontal range checking } If MnX<0 Then MnX:=0; If MxX>319 Then MxX:=319; { Draw the line } If MnX