Issue #4
Saturday, October 6th, 2001

Credits

Editor:
Fling-master

Writers/Contributors:
Plasma357
Singsong

Contents

And after a long time a new issue finally arrives! (Yayyy! *cheers*)

Sorry about the incredible delay this issue went through. I hope to make it up by getting some kick-ass articles and other stuff in this issue and the near future.

The articles contained within include part 3 of the RPG creation article, this one taking a closer look at working with NPCs. Also we've got partt 2 of the EMS article by Plasma357, a beginners SVGA tutorial, a beginners guide to assembly (part 1) and last but certainly not the least we have a rant for your reading pleasure. And of course we've got the usual assortment of news from around the QB community.

Also, in an attempt to prevent long delays between issues, I've decided to make the mag a bi-monthly publication. This means, for those who have never heard the term before, that we will be releasing a new issue once every other month. This gives me more time to write articles and work on my other projects and not to mention, school work.

Enjoy!

-Fling-master

The only letters I actually received about the mag were from people wondering what happened to it, and I don't deem it necessary to print those here. =)

 

Sorry about the lack of news this issue. Usually I keep a text file, that I open in Notepad when I surf the net and visit QB message boards and sites, but I seem to have lost the file I had started way back, after the last issue was released. Next time there'll be more news.

 

Sources
  • Future Software Message Board
  • QuickBASIC News
  • V-Planet
Big changes at qb45.com
For those who've been living under a rock for the last month or so, Future Software has a new webmaster. And the lucky individual is wildcard.

The QB Chronicles wishes wildcard the best of luck in maintaining the qb45.com domain.

Also before he sold the site, Jorden gave the oh-so-popular message board a complete overhaul. Now you can edit your own messages, send private messages, visit other boards (like the ASM board, and the General discussion board). Also your posts show up in orange so it's now super easy to locate replies to your own posts.

 

HyperRealistic Games down for the summer
HyperRealistic Games went offline for some time during the summer. This is because I (Fling-master) had gotten sick of Hypermart's picky banner ads and really wanted to merge the QBRPG page and the main site together but didn't have the webspace to do so.

Now the site is back up along with this site and the QBRPG Top 50.

 

US Terrorist Attacks having effects on the QB community
After the September 11th terrorist attacks on the World Trade Center in New York City and the Pentagon in Washington, many QB sites had posted notes of sympathy to the people affected (or who knew people who had been affected by) the attacks.

 

Thought you'd heard the last of Dark Ages II?
The once-mega popular sequel to one of QB's older and better RPGs is nearing completion. All the graphics are done, and the engine is complete. All that's left to be done is to design maps. No word yet on when the entire game will be actually released yet though, but next issue we'll try to get a preview of what the finished game will be like.

 

RPG Engine Advanced v1.3 released!
HyperRealistic Games has released RPG Engine Advanced v1.3. It uses DirectQB and features true pixel scrolling, multiple, walking, differently behaving NPCs, and the ability to shoot badguys and for them to shoot you! Full commented source is included.

 

Kingdoms 2 Released!

Kingdom's 2, a rocking fantasy wargame release by Piptol Productions was released earlier this month. This is one of the few wargames ever to be made in QB and it rocks! Get it now!

 

Pure QB Demo Contest 2001 Started

The Pure QB Demo Contest 2001, run by Toshihiro Horie, was started. Visit the site to learn the rules and what is expected. Basically, you got to program a pure QB (no asm or anything else) demo of some sort, that has some other constraints. Check it out.

 

UBasic Compiler Cancelled
To many people's disappointment (including the editor's), the UBasic compiler was cancelled. The author Gabriel Fernandez, has said that he is tired of programming it and will not continue working on it. Full source was posted at the UBasic project site.

 

 

Here's where you can vote for your favourite QB game, utility, demo, etc. If you've got a favourite then please vote below.

Submit your vote!

Vote for your favourite QB game, utility, demo, etc.

Name of program:

The results from this issues voting will go in here next issue =)

Welcome Back: A Brief Review

In part one, we set up a basic EMS interface with QuickBasic. We also created a sample program, which did nothing more than test the EMS routines and shuffle some memory around.

Note: If you missed part one, you should read it before continuing. It's available in the tutorials section of the Nemesis QB website. It's also published in issue #3 of the QB Chronicles.

Now we're going to get to the fun stuff: playing sounds stored in EMS!

Sound Blaster: Some More History

The Sound Blaster card (and every card compatible with it) supports two types of sound: FM synthesis sound and digitized sound.

Figure 2-1: FM synthesis sound

FM synthesis sound uses different parameters to control the sound, allowing the programmer to crudely "emulate" instruments. When you play a MIDI file in Windows (without a wavetable sound card), you are hearing FM synthesis. FM synthesis is used mainly for music, although it is sometimes used for sound effects.


Figure 2-2: Digitized sound

Digitized wave sound works by playing many sound "samples" each second, which produces sound. Any sound can be produced with a digitized wave, if the frequency (the number of samples/sec, aka "sampling rate") is high enough. This makes digitized sound a popular choice for speech and sound effects. The frequency usually ranges from 8,000 Hz to 44,100 Hz. Also, each sample can have an 8-bit or a 16-bit value. (Needless to say, a 16-bit sample is much more precise than an 8-bit sample.) When you play a WAV file from Windows, you are hearing digitized sound.

As you may have guessed, digitized sound has greater sound quality than FM synthesis. Besides that, FM synthesis is becoming outdated, and it tends to sound "cheesy" anyway. So we'll be using digitized sound in this tutorial.

So How Does EMS Help Us?

The main problem with digitized sound is that it takes up a lot of memory. One second of 16-bit stereo sound sampled at 44 KHz (that's 44,100 samples/sec, CD-quality) takes up 176K! However, most games can get by with 22 KHz sampling and 8-bit mono sound, which only takes 22 K/sec. That's still a lot, but it's easier to deal with.

Another reason DOS games use lower sampling rates and 8-bit sound is that most Sound Blaster clones are only compatible with earlier SB models (1, 2, or Pro) which don't support 16-bit sound (in DOS) or high sampling rates.

Even if we're using 22 KHz 8-bit mono sound, we're going to run out of memory quickly if we're limited to storing our sounds in the 640 K base memory. This is where EMS comes into play! With EMS, we have vast quantities of memory to store digitized sounds without wasting any base memory. We can even play back the sound directly from EMS without the need for a buffer in base memory.

Since this is an EMS tutorial and not a Sound Blaster tutorial, I'm not going to go into detail on the workings of the various Sound Blaster cards. We'll just use the basic functions that are compatible with all Sound Blaster cards. (But don't worry, I've got a mega Sound Blaster tutorial coming soon...)

Let's Get Started!

We will be using the EMS functions I presented in part 1 of this series. However, I'm not going to list them again; I will just add on to the code.

Note: You can find the complete source code for this tutorial in ems2.zip.

First, we'll need to define a few global variables and constants to store the sound settings and status.


DEFINT A-Z
'$DYNAMIC

DIM SHARED SB.BaseAddr  'Sound Blaster base address
DIM SHARED SB.DMAchan   'Sound Blaster DMA channel
DIM SHARED SB.DMApage   'DMA page register (set by SB.Init)
DIM SHARED SB.DMAadd    'DMA address register (set by SB.Init)
DIM SHARED SB.DMAlen    'DMA length register (set by SB.Init)
DIM SHARED SB.Sound     'Currently playing sound slot (set by SB.PlaySound)

CONST SB.MaxSounds = 9  'Maximum number of sound slots. This can be changed,
                        '  just remember that 64K is required for each slot.

'Holds the frequency, length, and priority of each sound slot.
DIM SHARED SB.SlotInfo(1 TO SB.MaxSounds, 1 TO 3) AS LONG

Detecting and Resetting the Sound Blaster

Before we can use the Sound Blaster card, we need to check to make sure it is present and initialize it. In order to do this, we need to know the base address and DMA channel (8-bit) of the card. The base address ranges from 210h to 260h, and the DMA channel ranges from 0 to 3. Because the user can change these settings, you can't be absolutely sure what they are.

There are two ways to find out: Ask the user, or look at the "BLASTER" environment variable

The BLASTER environment variable is set in the user's AUTOEXEC.BAT file. This variable contains the sound card settings in a string like so:


SET BLASTER=A220 I5 D1 T4

Warning: This variable may not exist. Even if it does, it may not have the correct settings if the user or another program has tampered with it!

The only two settings we are interested with are "A", the base address, (220h in the example above) and "D", the DMA channel (1 in the example above).


DECLARE SUB SB.GetConfig ()

SUB SB.GetConfig

  'Reads the BLASTER environment variable and gets the Sound Blaster base
  'address and 8-bit DMA channel from it.

  Config$ = UCASE$(ENVIRON$("BLASTER"))      'Get the variable from DOS

  FOR x = 1 TO LEN(Config$)                  'Look at each character in it
    SELECT CASE MID$(Config$, x, 1)
      CASE "A"                               'We found an "A", so the next 3
                                             '  characters are the base
                                             '  address in hex.
        SB.BaseAddr = VAL("&H" + MID$(Config$, x + 1, 3))

      CASE "D"                               'We found a "D", so the next
                                             '  character is the 8-bit DMA
                                             '  channel.
       SB.DMAchan = VAL(MID$(Config$, x + 1, 1))
    END SELECT
  NEXT

END SUB

If the BLASTER variable is not present, your only other option is to ask the user for the settings. If the user doesn't know, try 220h for the base address and 1 for the DMA channel (because these are the most common settings) and cross your fingers!

After we have the base address, we can determine the I/O register (aka port) addresses for the DSP chip on the sound card. The I/O registers are used to communicate with the DSP chip, which controls the sound card.

DSP I/O Register Addresses

Reset Base + 6h
Read Base + Ah
Write Base + Ch
Available Base + Eh

Before we can use the Sound Blaster, we need to reset the DSP chip. This is done by sending a "1" and a "0" to the reset register, then checking the read register for the value AAh. If the read register does not return AAh after 65,535 reads, then the base I/O address is either incorrect or no sound card is installed.

Since we also know the DMA channel, we can determine the channel-specific registers used to control DMA transfers: the page register, address register, and length register. (We will use different sets of registers depending on which DMA channel the sound card is using.)

DMA Channel-Specific Registers

Channel Page Address Length
0 87h 0h 1h
1 83h 2h 3h
2 81h 4h 5h
3 82h 6h 7h

The DMA controller also uses three general registers which are used no matter which DMA channel the sound card is using.

DMA General Registers

Ah Mask register
Bh Mode register
Ch Clear register

Before we do any DMA transfers, we should reset the DMA controller. This is done by setting the mask bit in the DMA mask register (channel + 4h), and then sending a "0" to the DMA clear register.

Below is the complete function to reset and initialize the Sound Blaster.


DECLARE FUNCTION SB.Init ()

FUNCTION SB.Init

  'Initializes the Sound Blaster by resetting the DSP chip, and determines
  'which DMA registers to use based on the selected DMA channel.
  '
  'The sound card configuration must be set (either by SB.GetConfig or
  'manually) prior to calling this function.
  '
  'If the DSP is successfully reset, this function will return -1. If the
  'DSP could not be reset or the DMA channel is invalid, it will return 0.

  OUT SB.BaseAddr + &H6, 1                  'Send a "1" to the DSP reset reg
  OUT SB.BaseAddr + &H6, 0                  'Send a "0" to the DSP reset reg

  FOR ResetDSP& = 1 TO 65535                'Wait up to 65,535 reads

    IF INP(SB.BaseAddr + &HA) = &HAA THEN   'DSP read reg returned AAh,
      EXIT FOR                              '  which means it has been reset.

    ELSEIF ResetDSP& = 65535 THEN           'Still no success after 65,535
      SB.Init = 0                           '  tries, so we must have the
      EXIT FUNCTION                         '  wrong base address or there is
    END IF                                  '  no Sound Blaster card.
  NEXT

  SELECT CASE SB.DMAchan      'Since we know which DMA channel the Sound
                              '  Blaster is using, we can set up the
                              '  channel-specific registers beforehand to
                              '  save a little time.

    CASE 0                    'DMA Channel 0 (8-bit)
      SB.DMApage = &H87       'Page register = 87h
      SB.DMAadd = &H0         'Address register = 0h
      SB.DMAlen = &H1         'Length register = 1h

    CASE 1                    'DMA Channel 1 (8-bit)
      SB.DMApage = &H83       'Page register = 83h
      SB.DMAadd = &H2         'Address register = 2h
      SB.DMAlen = &H3         'Length register = 3h

    CASE 2                    'DMA Channel 2 (8-bit)
      SB.DMApage = &H81       'Page register = 81h
      SB.DMAadd = &H4         'Address register = 4h
      SB.DMAlen = &H5         'Length register = 5h

    CASE 3                    'DMA Channel 3 (8-bit)
      SB.DMApage = &H82       'Page register = 82h
      SB.DMAadd = &H6         'Address register = 6h
      SB.DMAlen = &H7         'Length register = 7h

    CASE ELSE                 'The DMA channel is either 16-bit or invalid
      SB.Init = 0             'Return error status
      EXIT FUNCTION
  END SELECT

  OUT &HA, SB.DMAchan + &H4   'Reset the DMA controller by setting the mask
                              '  bit in the DMA mask register and clearing
  OUT &HC, &H0                '  any current transfers by sending a 0 to the
                              '  DMA clear register.

  SB.Sound = 1                'Set the last playing sound to 1
  SB.Init = -1                'Return the success code

END FUNCTION

Enabling the Sound

Even after we have reset the Sound Blaster, we still won't hear any sound until the "speaker" is turned on. (That's Creative-speak for enabling the sound output.) This should only be done once at the beginning of your program, since it causes the speakers to "click". Before your program ends, it should also turn the "speaker" back off.


DECLARE SUB SB.SpeakerOn ()
DECLARE SUB SB.SpeakerOff ()

SUB SB.SpeakerOn

  'Turns the "speaker" on. This actually just enables the digitized sound
  'output so that the sound can be heard.

  SB.WriteDSP &HD1

END SUB

SUB SB.SpeakerOff

  'Turns the "speaker" off. This actually just disables the digitized sound
  'output, effectively muting the sound.

  SB.WriteDSP &HD3

END SUB

Loading Sounds into EMS

Once the Sound Blaster is all reset and initialized, we are ready to play some sounds! The only problem is, we don't have any sounds to play!

Figure 2-3: Four pages are allocated to each sound slot

The easiest way to get sounds is to load them in from WAV files. We'll designate a different "slot" for each sound when we load them. Using this method, we can load multiple sounds and play them back by telling our playback sub which slot to play.

Because DMA transfers can only handle 64K at a time, each sound will be limited to 64K in length. The sounds will also be limited to 22 KHz 8-bit so our routines will work on all Sound Blaster cards.

There is one more limitation: We can only play one sound at a time. This is because the Sound Blaster only has one digitized sound channel. However, we can use a priority-based sound system to improve this a bit.

If you remember from part one, each EMS page is 16K. So we'll allow each sound slot 4 pages (16 x 4 = 64K). This might waste a little memory, but it will be much more simpler than trying to dynamically allocate pages to each sound based on the sound's size!

So how does one load a wave file? Well, it's just like any other binary file! The only thing we have to be concerned with is making sure that the sound is 8-bit, 23 KHz or less, and 64K or less in length. Luckily, all this information is stored in a "header" in the wave file.

Microsoft Wave File Format

Offset Length Type Description
0 4 STRING RIFF ID string. Must be "RIFF".
4 4 LONG RIFF length. We don't care about this.
8 4 STRING Wave ID string. Must be "WAVE".
12 4 STRING Format ID string. Must be "fmt", plus a space.
16 4 LONG Format length. We don't care about this.
20 2 INT Format tag. This should be 1, which is 8-bit PCM. If it is something else, then the wave file is probably 16-bit or ADPCM compressed. Either way, we can't use it!
22 2 INT Number of Channels. This should be 1, which is mono sound. It may also be 2 or 4, for stereo or quad sound. However, we can only play mono sounds.
24 4 LONG Frequency. Remember, we can only play wave files that are 23 KHz or less.
28 4 LONG Transfer rate. We don't care about this.
32 2 INT Block alignment. This should be 1 for 8-bit mono files.
34 2 INT Reserved for other format information.
36 4 STRING Data ID. Must be "data".
40 4 LONG The data length. We can only play wave files 64K or less. So we'll only play the first 64K if the file is longer.
44 ? ? The actual wave data. This is what gets loaded into EMS.

The only problem left is the question of getting the actual wave data into EMS. We could read the data directly from the file into EMS (if the correct pages were mapped to the page frame). However, this would require use to use DOS interrupts for file transfers, which can get messy. But don't worry, there's another way!

We can create a small buffer (1K or so) in base memory. Then QuickBasic can read the file 1K at a time and put that into the buffer (which is actually a 1K string variable). From there, we can use our EMS.CopyMem routine from part 1 to copy the data from the buffer to a logical page EMS. Then the program just loops around and gets another 1K until the whole file has been loaded.

After the sound has been loaded into EMS, we'll need to save the frequency and length of the sound so we can play it back later. We'll also save the sound's priority (more on this later). We can use the shared SB.SlotInfo array defined at the beginning of our program to do this.


DECLARE FUNCTION SB.LoadSound (Filename$, Slot, Priority, Handle)

FUNCTION SB.LoadSound (Filename$, Slot, Priority, Handle)

  'Loads a sound from a wave file into a sound "slot" in EMS, where:
  '
  'Filename$ = Filename of an 8-bit mono PCM wave file <=23 KHz. If the file
  '            is larger than 64k, only the first 64k will be loaded.
  'Slot      = Sound slot to load the sound into
  'Priority  = Priority to assign to the sound
  '            (For example, 1 = #1 priority, 2 = #2 priority, etc.)
  'Handle    = EMS handle of memory to store sounds in
  '
  'Returns true (-1) if the sound was loaded successfully
  'or false (0) if there was an error.

  WaveFile = FREEFILE
  OPEN Filename$ FOR BINARY AS #WaveFile

  IF LOF(WaveFile) = 0 THEN       'The file length is 0, so assume that
    CLOSE #WaveFile               '  we created the file when we opened it
    KILL Filename$                '  and delete it.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  RiffID$ = SPACE$(4)
  GET #WaveFile, , RiffID$        'Check the RIFF ID string. If it's not
  IF RiffID$ <> "RIFF" THEN       '  "RIFF", then the wave file is invalid.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , RiffLen&       'Get the RIFF length and ignore it :)

  WaveID$ = SPACE$(4)
  GET #WaveFile, , WaveID$        'Get the wave ID string. If it's not
  IF WaveID$ <> "WAVE" THEN       '  "WAVE", then the wave file is invalid.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  FormatID$ = SPACE$(4)
  GET #WaveFile, , FormatID$      'Get the format ID string. If it's not
  IF FormatID$ <> "fmt " THEN     '  "fmt ", then the wave file is invalid.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , FormatLen&     'Get the format length and ignore it

  GET #WaveFile, , FormatTag      'Get the format tag, which defines what
                                  '  format the data is in. This needs to be
                                  '  "1", which is uncompressed PCM.
  IF FormatTag <> 1 THEN          'If it's something else, we can't play it
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , NumChannels    'Get the # of channels. This needs to be "1"
                                  '  (because we can only play mono sounds)
  IF NumChannels <> 1 THEN        'If it's stereo, we can't play it
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , Frequency&     'Get the sound frequency (sampling rate)
  IF Frequency& > 23000 THEN      'We can't play sounds > 23 KHz
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , TransferRate&  'Get the data transfer rate and ignore it

  GET #WaveFile, , BlockAlign     'Get the block alignment.
  IF BlockAlign <> 1 THEN         'If it's not "1", then it's not an 8-bit
                                  '  wave and we can't play it.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , ExtraData      'Get the extra data and ignore it

  DataID$ = SPACE$(4)
  GET #WaveFile, , DataID$        'Get the data ID string. If it's not
  IF DataID$ <> "data" THEN       '  "data", then the wave file is invalid.
    SB.LoadSound = 0              'Return an error code
    EXIT FUNCTION
  END IF

  GET #WaveFile, , DataLen&                  'Get the sound data length
  IF DataLen& > 65536 THEN DataLen& = 65536  'If the sound is greater than
                                             '  64K, load only the first 64K


  BufferSize = 1024              'Set the buffer size to 1K. For faster
  Buffer$ = SPACE$(BufferSize)   'loading, you can increase the buffer size.
                                 '(Try 4096 to 16384). However, if the
                                 'buffer is too large you may run out of
                                 'string space in your program.

  DataRead& = 0                  'We haven't read any data yet...

  DO
    IF DataRead& + BufferSize > DataLen& THEN  'If the buffer is larger than
                                               '  the data left to read, we
      Buffer$ = SPACE$(DataLen& - DataRead&)   '  need to adjust it so we
                                               '  don't read past the end
    END IF                                     '  of the file.

    GET #WaveFile, , Buffer$                   'Read the data into the buffer

    Page = DataRead& \ 16384                   'Determine the EMS page and
    Offset = DataRead& - Page * 16384&         '  offset to load the data
                                               '  into.

    Page = Page + (Slot - 1) * 4               'Adjust the page depending on
                                               '  which slot we're using.

    'Copy the data from the buffer to EMS
    EMS.CopyMem LEN(Buffer$), 0, VARSEG(Buffer$), SADD(Buffer$), Handle, Page, Offset

    DataRead& = DataRead& + LEN(Buffer$)       'Increase the number of bytes
                                               '  read by the size of the
                                               '  data buffer.

  LOOP UNTIL DataRead& = DataLen&              'Loop around until all the
                                               '  data has been loaded.

  Buffer$ = ""        'Set the buffer to null to restore the string space

  CLOSE #WaveFile

  SB.SlotInfo(Slot, 1) = Frequency&    'Save the frequency,
  SB.SlotInfo(Slot, 2) = DataLen&      '  length,
  SB.SlotInfo(Slot, 3) = Priority      '  and priority of the sound.

  SB.LoadSound = -1                    'Sound loaded successfully

END FUNCTION

So What is this "Priority" Thing?

Since the Sound Blaster can only play one sound at a time (in hardware), we will be using a priority-based system. Using this system, each sound is assigned a priority when it is loaded. Before we play a new sound, we check to see if another sound is already playing. If so, we check the priority of the currently playing sound. If the sound currently playing is more important, we don't interrupt it. But if the new sound has the same priority or is more important, we stop the currently playing sound and play the new sound.

The following function will tell us whether or not the Sound Blaster is playing a sound by checking to see if the DMA channel is in use. We can use this function with our SB.PlaySound sub to implement the priority-based system.


DECLARE FUNCTION SB.InUse ()

FUNCTION SB.InUse

  'Returns true (-1) if the Sound Blaster DMA channel is in use
  'or false (0) if it's not.

  OUT &HC, &H0                  'Send 0h to the DMA clear register

  'Get the number of bytes left to be transferred from the DMA length
  'register. Since registers are 8-bit, we need to read the low byte first
  'and then read the high byte.
  BytesLeft& = INP(SB.DMAlen) + INP(SB.DMAlen) * 256&

  IF BytesLeft& = &HFFFF& OR BytesLeft& = 0 THEN  'When the DMA controller is
                                                  '  not transferring data,
                                                  '  it will return either
                                                  '  FFFFh or 0.

    SB.InUse = 0                'No data is being transferred
  ELSE
    SB.InUse = -1               'Data is still being transferred
  END IF

END FUNCTION

Playing Sounds

Once a sound has been loaded, it is ready to be played. To play a sound, we need to know four things: the priority, frequency, length, and memory location.

We stored the priority, frequency, and length in the SB.SlotInfo array when we loaded the sound, but what about the memory location? Since we can only perform DMA transfers from the 1 MB memory region, the EMS logical pages containing the sound must be mapped to the page frame first. Then we can use the page frame address for the memory location.

So how do we know which logical pages to map to the page frame? Well, because each sound slot is 4 pages, we can use:


EMS.MapXPages 0, (Slot - 1) * 4, 4, Handle

(We have to subtract 1 from the slot number because the sound slots are numbered starting at 1, while the EMS pages are numbered starting from 0.)

Figure 2-4: Mapping the sound to play to the page frame

Once the correct pages have been mapped, we can program the DMA controller to transfer the sound from memory to the sound card.

  1. Mask the DMA channel which we will be using by setting the mask bit in the DMA mask register. This can be done by adding 4 to the DMA channel number and sending that value to the DMA mask register.

  2. Send a 0 to the DMA clear register to stop any current transfers.

  3. Set the DMA transfer mode to output by adding 48h to the DMA channel number and sending it to the DMA mode register.

  4. Set the address and page.

  5. Set the length to transfer.

  6. Clear the mask bit on the mask register. The DMA transfer will begin as soon as the Sound Blaster is ready.

After programming the DMA controller, the only thing left to do is tell the Sound Blaster DSP that we want to play a sound. But to do this we have to wait until the DSP is ready and then send the commands. This sub checks the DSP write register and waits until the DSP is ready before sending the byte.


DECLARE SUB SB.WriteDSP (Byte)

SUB SB.WriteDSP (Byte)

  'Writes the value [Byte] to the DSP write register.

  DO
    Ready = INP(SB.BaseAddr + &HC)  'Wait until the DSP is ready
  LOOP WHILE Ready AND &H80         '  to accept the data

  OUT SB.BaseAddr + &HC, Byte       'Send the value

END SUB

  1. Tell the DSP that we want to set the digitized sound transfer time constant by sending 40h.

  2. Send the time constant: 256 - 1,000,000 \ Frequency

  3. Tell the DSP that we want to output a sound using 8-bit single-cycle DMA by sending 14h.

  4. Send the length of the sound.

The Sound Blaster will begin playing the sound immediately after step 4 is complete. And since we are using DMA to play back the sound, our program can do whatever it wants while the sound is playing. That's right, the sounds are played in the background!


DECLARE SUB SB.PlaySound (Slot, Handle)

SUB SB.PlaySound (Slot, Handle)

  'Plays a sound from a "slot" in EMS, where:
  '
  'Slot = Sound slot to play
  'Handle = EMS handle of memory sound is stored in

  'If a sound is already playing with a higher priority, don't interrupt it.
  IF SB.InUse AND SB.SlotInfo(SB.Sound, 3) < SB.SlotInfo(Slot, 3) THEN
    EXIT SUB
  END IF

  SoundFreq& = SB.SlotInfo(Slot, 1)             'Get the sound frequency
  SoundLen& = SB.SlotInfo(Slot, 2) - 1          'Get the sound length

  Address& = (&H10000 + EMS.PageFrame) * 16&    'Calculate the 20-bit address
  Page = (&H10000 + EMS.PageFrame) / &H1000     '  and page of the EMS page
                                                '  frame.

  EMS.MapXPages 0, (Slot - 1) * 4, 4, Handle    'Map the sound (64K, 4 pages)
                                                '  to the EMS pageframe.

  'Program the DMA controller
  OUT &HA, SB.DMAchan + &H4                     'Mask the DMA channel to use
                                                '  by setting the mask bit in
                                                '  the DMA mask register.

  OUT &HC, &H0                                  'Clear any current transfers
                                                '  by sending a 0 to the DMA
                                                '  clear register.

  OUT &HB, SB.DMAchan + &H48                    'Set the transfer mode to
                                                '  "output" with the DMA mode
                                                '  register.

  OUT SB.DMAadd, Address& AND &HFF               'Set the low and
  OUT SB.DMAadd, (Address& AND &HFF00&) \ &H100  '  high byte of the address.

  OUT SB.DMApage, Page                             'Set the page.

  OUT SB.DMAlen, SoundLen& AND &HFF                'Set the low and
  OUT SB.DMAlen, (SoundLen& AND &HFF00&) \ &H100   '  high byte of the sound
                                                   '  length.

  OUT &HA, SB.DMAchan       'Clear the mask bit in the DMA mask register


  'Program the DSP chip
  SB.WriteDSP &H40                         'Select the "set time transfer
                                           '  constant" function.

  SB.WriteDSP 256 - 1000000 \ SoundFreq&   'Calculate and send the constant.

  SB.WriteDSP &H14                         'Select the "8-bit single-cycle
                                           '  DMA output" function.

  SB.WriteDSP SoundLen& AND &HFF                 'Send the low and
  SB.WriteDSP ((SoundLen& AND &HFF00&) \ &H100)  '  high byte of the
                                                 '  sound length

  SB.Sound = Slot  'Save the slot number of the currently playing sound

END SUB

Pausing the Sound

Although the sound is playing in the background, it can be paused at any time. To do this, all we have to do is send D0h to the DSP write register. This will pause any sound that is currently playing for an indefinite amount of time.

To resume a paused sound, we just send D4h to the DSP write register. The sound will then continue playing where it left off.


DECLARE SUB SB.Pause ()
DECLARE SUB SB.Resume ()

SUB SB.Pause

  'Pauses the sound currently playing

  SB.WriteDSP &HD0

END SUB

SUB SB.Resume

  'Resumes the sound currently playing

  SB.WriteDSP &HD4

END SUB

Another Sample Program

Here is a sample program I wrote to test out all the Sound Blaster routines presented in this tutorial. The sample program simply loads 9 wave files into EMS and allows you to play them back by pressing 1-9. It also demonstrates the priority-based sound system by assigning different priorities to each sound. Check it out!


COLOR 7
CLS
IF NOT EMS.Init THEN
  PRINT "No EMM detected."
  END
END IF

SB.GetConfig
IF SB.BaseAddr = 0 OR SB.DMAchan = 0 THEN
  PRINT "Sound Blaster settings not found. Please enter them manually."
  PRINT
  INPUT "Base address (usually 220): ", BaseAddr$
  SB.BaseAddr = VAL("&H" + BaseAddr$)
  INPUT "DMA channel (usually 1): ", SB.DMAchan
  PRINT
END IF

IF NOT SB.Init THEN
  PRINT "No Sound Blaster detected."
  END
END IF

CLS
COLOR 14, 1
PRINT SPACE$(22); "Using EMS in QuickBasic: Part 2 of 3"; SPACE$(22)
COLOR 15, 0
PRINT STRING$(31, 196); " EMS Information "; STRING$(32, 196)
COLOR 7
PRINT "EMM Version: "; EMS.Version$

IF EMS.Version$ < "4.0" THEN
  PRINT
  PRINT "EMM 4.0 or later must be present to use some of the EMS functions."
  END
END IF

PRINT "Page frame at: "; HEX$(EMS.PageFrame); "h"
PRINT "Free handles:"; EMS.FreeHandles

IF EMS.FreeHandles = 0 THEN
  PRINT
  PRINT "You need at least one free handle to run this demo."
  END
END IF

PRINT "Total EMS:"; EMS.TotalPages; "pages /"; EMS.TotalPages * 16&; "KB /"; EMS.TotalPages \ 64; "MB"
PRINT "Free EMS:"; EMS.FreePages; "pages /"; EMS.FreePages * 16&; "KB /"; EMS.FreePages \ 64; "MB"

IF EMS.FreePages < 36 THEN
  PRINT
  PRINT "You need at least 36 pages (576 KB) free EMS to run this demo."
  END
END IF

PRINT
COLOR 15
PRINT STRING$(26, 196); " Sound Blaster Information "; STRING$(27, 196)
COLOR 7
PRINT "Base Address: "; HEX$(SB.BaseAddr); "h"
PRINT "DMA Channel:"; SB.DMAchan
PRINT
COLOR 15
PRINT STRING$(30, 196); " Setting Up Sounds "; STRING$(31, 196)
COLOR 7
PRINT "Allocating 36 pages (576 KB) of EMS...";

Handle = EMS.AllocPages(64)
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok! (Using handle "; LTRIM$(STR$(Handle)); ")"
END IF

FOR Sfx = 1 TO 9

  PRINT "Loading SFX"; LTRIM$(STR$(Sfx)); ".WAV into slot "; LTRIM$(STR$(Sfx)); "...";

  SELECT CASE Sfx
    CASE 1, 3, 9
      Priority = 1
    CASE 4, 7, 8
      Priority = 2
    CASE 5, 6
      Priority = 3
    CASE 2
      Priority = 4
  END SELECT

  IF NOT SB.LoadSound("SFX" + LTRIM$(STR$(Sfx)) + ".WAV", Sfx, Priority, Handle) THEN
    PRINT "error!"
    PRINT "couldn't load wave file"
    END
  ELSE
    PRINT "ok! ("; LTRIM$(STR$(SB.SlotInfo(Sfx, 1))); " Hz,"; SB.SlotInfo(Sfx, 2); "bytes, #"; LTRIM$(STR$(SB.SlotInfo(Sfx, 3))); " priority)"
  END IF

NEXT

LOCATE 25, 28
COLOR 31
PRINT "Press any key to proceed";

KeyPress$ = INPUT$(1)
COLOR 7
CLS
COLOR 14, 1
PRINT SPACE$(22); "Using EMS in QuickBasic: Part 2 of 3"; SPACE$(22)
COLOR 15, 0
PRINT STRING$(34, 196); " Sound Test "; STRING$(34, 196)
COLOR 7
PRINT
PRINT "Here you can test the sound playback routines. In this demo, only 9 sounds"
PRINT "have been loaded into EMS. However, in your own programs you may load as many"
PRINT "sounds as you want! The only limitation is that you must have enough free EMS."
PRINT "(And I don't think that should be a problem... ^_^)"
PRINT
COLOR 15
PRINT "Press 1 through 9 to hear different sounds."
PRINT
PRINT "Press P to pause/stop sound playback and R to resume a paused sound."
PRINT
PRINT "Press ESC to end the demo."
PRINT
COLOR 7
PRINT "For example, press 2 to hear a long drone. After the sound has started, press"
PRINT "P and the sound will pause. You can then play a different sound or resume the"
PRINT "same sound by pressing R."
PRINT
PRINT "You will also notice that some sounds have a higher priority than other sounds."
PRINT "For instance, sound 9 has a #1 priority while sound 4 has a #2 priority. This"
PRINT "means that sound 9 will interrupt sound 4 if it is playing. However, if sound 9"
PRINT "is playing, sound 4 will not interrupt it (because sound 9 has a lower priority)"

SB.SpeakerOn

COLOR 15
DotPos = 1
DotDir = 1
DO
  KeyPress$ = UCASE$(INKEY$)
  SELECT CASE KeyPress$
    CASE "1" TO "9"
      SB.PlaySound VAL(KeyPress$), Handle
    CASE "P"
      SB.Pause
    CASE "R"
      SB.Resume
  END SELECT

  IF SB.InUse THEN
    Sound$ = LTRIM$(STR$(SB.Sound))
    Freq$ = LTRIM$(STR$(SB.SlotInfo(SB.Sound, 1)))
    Size$ = LTRIM$(STR$(SB.SlotInfo(SB.Sound, 2)))
    Priority$ = LTRIM$(STR$(SB.SlotInfo(SB.Sound, 3)))
    LOCATE 25, 1
    COLOR 15
    PRINT "Playing sound #"; Sound$; " ("; Freq$; " Hz, "; Size$; " bytes, #"; Priority$; " priority)"; SPACE$(12);
    DotCol = 10
  ELSE
    LOCATE 25, 1
    COLOR 15
    PRINT "No sound is playing"; SPACE$(40);
    DotCol = 12
  END IF

  WAIT &H3DA, 8, 8
  WAIT &H3DA, 8

  LOCATE 24, DotPos
  COLOR DotCol
  PRINT CHR$(254);
  IF DotPos > 1 THEN
    LOCATE 24, DotPos - 1
    PRINT " ";
  END IF
  IF DotPos < 80 THEN
    LOCATE 24, DotPos + 1
    PRINT " ";
  END IF
  IF DotDir = 1 THEN
    DotPos = DotPos + 1
    IF DotPos = 81 THEN
      DotPos = 80
      DotDir = 2
    END IF
  ELSEIF DotDir = 2 THEN
    DotPos = DotPos - 1
    IF DotPos = 0 THEN
      DotPos = 1
      DotDir = 1
    END IF
  END IF

LOOP UNTIL KeyPress$ = CHR$(27)

SB.SpeakerOff

COLOR 7
CLS
COLOR 14, 1
PRINT SPACE$(22); "Using EMS in QuickBasic: Part 2 of 3"; SPACE$(22)
COLOR 15, 0
PRINT STRING$(33, 196); " Ending Demo "; STRING$(34, 196)
COLOR 7
PRINT
PRINT "Deallocating 36 pages...";

EMS.DeallocPages (Handle)
IF EMS.Error THEN
  PRINT "error!"
  PRINT EMS.ErrorMsg$
  END
ELSE
  PRINT "ok!"
END IF

END

That's All for Now...

Well, I hope you enjoyed part 2 of the EMS tutorial series. Heck, maybe you even learned something! In part 3 (coming in a month or two), I'll finish up the series by showing you how to store sprites and huge data arrays in EMS, as well as some neat effects like screen scrolling and page flipping using EMS.

If you have any questions or comments about this article, or (gasp!) found an error, please e-mail me at plasma357@hotmail.com or post a message on the Nemesis QB messageboard.

This article was written by: Plasma357 - http://www.nemesisqb.com

I actually did have a scripting article written for this issue, but I decided to leave it until next issue since it wasn't very good, and becuase I really wanted to talk more about NPCs. Next issue there'll be a huge article on scripting.

So, last issue we talked about NPCs and how to move them around and display them. Well, that's all fine and dandy but now lets do some cool things with them. What cool things can we do with NPCs you say? Plenty! Today we'll talk about moving NPCs to a specific point on the map, and making an NPC go after the player when the player is within a certain distance of the NPC. This will provde a good basis for a real-time battle engine which we'll get into at a later point. (BTW if you want to see a version of the engine we're making in this series in action download mine. It uses DirectQB and runs pretty darn fast.)

So lets start by adding to our NPC data type:

TYPE NPCType                   'Holds NPC data
 x         AS INTEGER          'X coord
 y         AS INTEGER          'Y coord
 Dir       AS INTEGER          'The direction
 ImgStart  AS INTEGER          'The index into the tileset to display NPC
 Moving    AS INTEGER          'If its moving and the amount left
 Active    AS INTEGER          'Is it active?
 Speed     AS INTEGER          'The NPC's walking speed (in pixels)
 AI        AS INTEGER          'Type of movement
 xTarget   AS INTEGER          'Target position to walk to
 yTarget   AS INTEGER          '(Must not be set to aiRandom for this to be used)
 Range     AS INTEGER          'Amount of pixels before the NPC "sees" the player
END TYPE

'Also add this constants to because they'll be used by the new UpdateNPCs sub
CONST aiRandom = 0
CONST aiTarget = 1
CONST aiRange = 2

Just to go over some things we've added: ImgStart can be used to display NPCs which look different from each other, AI is the type of AI the NPC uses, xTarget and yTarget are the position the NPC is moving to. Range is basically the length in pixels of the NPC's vision.

Ok, now we're going to recode our whole UpdateNPCs sub because the one I gave you last time was horribly sloppy. (Well not horribly...) Just replace your sub with this code and I'll explain it all afterwards:

SUB UpdateNPCs
 'Go through all the NPCs
 FOR i = 0 TO Engine.MaxNPCs
  'Only update if the NPC is active
  IF NPC(i).Active = 0 THEN GOTO StartIt

  'If the NPC isn't currently moving then see if it "wants" to move
  IF NPC(i).Moving = 0 AND NPC(i).AI = aiRandom THEN
	IF INT(RND * 35) + 1 = 5 THEN
	 'Yes it does "want" to move, so move it
	 MoveNPC i, INT(RND * 4) + 1, 16
	ELSE
	 GOTO StartIt
	END IF
 
  'Check and see if it should move after a target
  ELSEIF NPC(i).Moving = 0 AND NPC(i).AI = aiTarget THEN
	'Get the target position for the NPC
	targetX = NPC(i).targetX
	targetY = NPC(i).targetY
	
	'Get distance
	farX = ABS(targetX - NPC(i).x) > 16
	farY = ABS(targetY - NPC(i).y) > 16

	'Decide which direction to move and move it
	IF targetX > NPC(i).x AND farX THEN MoveNPC i, East, 16
	IF targetX < NPC(i).x AND farX THEN MoveNPC i, West, 16
	IF targetY > NPC(i).y AND farY THEN MoveNPC i, South, 16
	IF targetY < NPC(i).y AND farY THEN MoveNPC i, North, 16

  'Check and see if it goes after the player if the player is close by
  ELSEIF NPC(i).Moving = 0 AND NPC(i).AI = aiRange THEN
	'See if the player is within a certain range of the NPC
	IF Engine.x >= (NPC(i).x - NPC(i).Range) AND Engine.x <= (NPC(i).x + NPC(i).Range) AND Engine.y >= (NPC(i).y - NPC(i).Range) AND Engine.y <= (NPC(i).y + NPC(i).Range) THEN

	 'Set a target (the player)
	 targetX = Engine.x
	 targetY = Engine.y
 
	 'Get distance
	 farX = ABS(targetX - NPC(i).x) > 16
	 farY = ABS(targetY - NPC(i).y) > 16
 
	 'Decide which direction to move and move it
	 IF targetX > NPC(i).x AND farX THEN MoveNPC i, East, 16
	 IF targetX < NPC(i).x AND farX THEN MoveNPC i, West, 16
	 IF targetY > NPC(i).y AND farY THEN MoveNPC i, South, 16
	 IF targetY < NPC(i).y AND farY THEN MoveNPC i, North, 16

	'We aren't withing range so do random movement
	ELSE
	 IF INT(RND * 35) + 1 = 5 THEN
	  'Yes it does "want" to move, so move it
	  MoveNPC i, INT(RND * 4) + 1, 16
	 ELSE
	  GOTO StartIt
	 END IF

	END IF
  END IF

  'If the NPC is moving then move the NPC
  IF NPC(i).Moving > 0 THEN
   NPC(i).Moving = NPC(i).Moving - 1
   SELECT CASE NPC(i).Dir
    CASE North
     NPC(i).y = NPC(i).y - NPC(i).Speed
    CASE South
     NPC(i).y = NPC(i).y + NPC(i).Speed
    CASE East
     NPC(i).x = NPC(i).x + NPC(i).Speed
    CASE West
     NPC(i).x = NPC(i).x - NPC(i).Speed
   END SELECT
  END IF
StartIt:
 NEXT i
END SUB

What this does is it updates all the NPCs which are currently active or 'alive'. It then checks what kind of AI we will use for the NPC. If we're doing random movement (aiRandom) then we see if it 'wants' to move and if so we move it 16 pixels in a random direction. If it is doing target based movement (aiTarget), then we must figure out in which direction we need to move in to get closer to the target. Once that is decided we move the NPC 16 pixels in the correct direction. The problem with this is that NPC's can get stuck behind walls when you're on the other side. A solution is to allow the NPCs to move diagonally, then any given NPC can 'slide' along the wall to get to their destination. Another problem we encounter is that this isn't 'smart' movement. i.e. There is not path-finding like in Diablo. The NPC simply goes in a straight line toward its target and doesn't try to avoid obstacles. We might get into that later, but for our purposes now this is not really a huge problem.

The last kind of AI for the NPCs is aiRange. This is where the NPC does random movement until the player is close by. When the player is within a certain amount of pixels the NPC will run after him/her. It uses the same method as aiTarget to go after the player. Keep in mind though, that to obtain maximum speed we are checking if the player is within a rectangle orginating at the NPC in question. A better way is to check the circular around the NPC but since this would involve multiplications and square roots (which are really slow) I've done it this way.

Now you're probably wondering where the heck the MoveNPC sub came from. Well this is a better structured way of moving NPCs than what I showed you last time. This way, we can move any given NPC by any given amount, anytime we want without having to insert lines of code to check for collision. Here is the sub:

SUB MoveNPC (num, Dir, amount)
 'Set the amount to move and the direction
 NPC(num).Moving = amount
 NPC(num).Dir = Dir

 'Now figure out which way to move
 SELECT CASE NPC(num).Dir
  CASE North
	'Check for collisions
	tile = Map(NPC(num).x \ 16, (NPC(num).y - 1) \ 16).collision
	tile2 = Map((NPC(num).x + 15) \ 16, (NPC(num).y - 1) \ 16).collision
	IF tile <> 1 AND tile2 <> 1 THEN
	ELSE
	 NPC(num).Moving = 0
	END IF
  CASE South
	'Check for collisions
	tile = Map(NPC(num).x \ 16, (NPC(num).y + 16) \ 16).collision
	tile2 = Map((NPC(num).x + 15) \ 16, (NPC(num).y + 16) \ 16).collision
	IF tile <> 1 AND tile2 <> 1 THEN
	ELSE
	 NPC(num).Moving = 0
	END IF
  CASE West
	'Check for collisions
	tile = Map((NPC(num).x - 1) \ 16, NPC(num).y \ 16).collision
	tile2 = Map((NPC(num).x - 1) \ 16, (NPC(num).y + 15) \ 16).collision
	IF tile <> 1 AND tile2 <> 1 THEN
	ELSE
	 NPC(num).Moving = 0
	END IF
  CASE East
	'Check for collisions
	tile = Map((NPC(num).x + 16) \ 16, NPC(num).y \ 16).collision
	tile2 = Map((NPC(num).x + 16) \ 16, (NPC(num).y + 15) \ 16).collision
	IF tile <> 1 AND tile2 <> 1 THEN
	ELSE
	 NPC(num).Moving = 0
	END IF
 END SELECT

END SUB

As you can see, it checks for collisions also.

Like I said at the beginning of the article, you can download a working copy of the engine we're building at HyperRealistic Games. It supports everything we've covered, but also allows the player (and NPCs) to fire bullets at each other. It uses DirectQB but could be ported to any other library with minimal effort.

Next issue there will be a mega-article on adding a script interpreter to your RPG engine. We'll create a scripting language which allows you to use variables (integer and string), do IF's, handle NPC conversation, change the map layout, and much, much more. Do not miss it!

This article was written by: Fling-master - http://www.qbrpgs.com

As most QB programmers know, the future of QB programs will rely more and more on the use of assembly code to boost the performance of their programs. Assembly is not an easy thing to learn. One look at assembly code can cause quite a strain on your brain if you never used it before. But, like any other language, if you use it enough, it will be very easy for you to use and understand, and given time you will be able to just glance at some code and be able to understand what it does.

This is the first in a series of articles that will hopefully help you to understand and succesfully program in assembly. Don't expect to learn this overnight though. You must use it constantly to get good at it. Just like when you first learned BASIC. The best way to learn a new language is by doing things in it and not just by reading a tutorial such as this. Plus you should actually WANT to learn assembly. If you don't really want to then stop now.

Alrighty then. First off, why even use assembly code? It's an old language created sometime in the 50's and 60's so why should we bother with it? Well it's really fast and it can allow you to do things you normally can't do in QB (such as SVGA programming). But before you start programming your entire game in assembly code, just remember that you should really only use it for things that need to be done quickly and things that your programming language can't do.

Well that's all fine you say, but how do you use it in QB? Well to do this you basically get to choose one of two methods. The first method involves writing your assembly code in a text file then assembling it into an OBJ file with an assembler such as TASM, MASM, NASM, a86, etc. Then you link it into a library file and load that library into QB! This is the preferred method and I will show you how to do this.

The other method is the only way to use assembly in QBasic 1.1. It involves using CALL ABSOLUTE. First you translate your assembly code into straight machine language and store it in a string. Then you pass the address of the string to CALL ABSOLUTE along with your parameters and presto! But this requires you to find an assembler that can produce raw machine code (DEBUG can do this). Plus you waste program memory storing the code in strings. Some old libraries like Blast! used this method.

For this assembly series I will show you how to use the first method using the TASM assembler.

In assembly you will make alot of use out of the memory. You should be familiar with addressing positions in memory as well. Conventional memory is split up into chunks called segments. These are 16 bytes in size. You always address memory with a segment/offset pair. Say you wanted to access memory position 18. You would use a segment of 1 (16 bytes) and an offset of 2. (16+2=18). Here's another example. To access memory position 37 you would use a segment of 2 and an offset of 5. Simple.

Some data types you should be familiar with are the bit, byte, word, and double word.

A bit is the smallest form of data a computer can use. It is either on (1) or off (0).

A byte is 8 bits long. It is the amount of memory that computers use to store on character such as the letter A. When a byte is unsigned the largest value it can hold is 255. The smallest being 0. When a byte is signed the largest value it can hold is 127 and the smallest is -128.

A word is two bytes long or 16 bits. When it's unsigned it can hold values in the range 65535 to 0. When it's signed 32767 to -32768 (This is the same as QB's integer).

A double word is 4 bytes in length or 32 bits. When it's signed it can hold 4294967296 to 0 When it's signed 2147483647 to -2147483648 (This is the same as QB's long integer).

Your computers CPU contains a bunch of little registers. If you computer is a 386+ then the registers are 32-bits long. If your computer is a 286 or below then the registers are 16-bits long. This article will assume you have 32-bit registers =). Registers are places for the CPU to store things. There are similar to variables. You can add, subtract, multiply, and divide them. But you have to be careful when using them because you can easily run out! There are only a few. Here are most of the registers:

AX, BX, CX, DX	-These are the mostly used ones. They can be used for just about anything.

CS		-This points to the current code segment
DS		-This points to the data segment
ES		-This is an extra segment register that can be used for a bunch of stuff
SS		-This points to the stack's segment
FS		-Similar to ES
GS		-Same as above
IP		-Instruction pointer. This is used with CS
SP		-Stack pointer. This is used with SS
BP		-Base pointer (points to the base of the stack)
SI		-Used mainly with DS
DI		-Used mainly with ES

A pointer is a variable/register that holds the address of something. You'll encounter these alot in C/C++. FS and GS are really used in protected mode but they can be used like ES. I've never had to use them though. Also I wouldn't modify CS, SS, IP, or SP. CS and IP point to the current instruction being executed and even if they could be changed, it would crash your computer. SS and SP point to the stack, which your computer uses constantly. It is alright to USE the values but you should never try to change them. Also ES and DS can't be assigned an immediate number. You must move a number into another register then move it into ES or DS.

Some registers of interest there are ES, DS, SI, and DI. ES and DI are a segment and offset register pair. ES stores the segment and DI stores the offset of something you want to access in RAM. The same goes with DS and SI. When you access what is being pointed to by ES, the computer will use DI as the offset by default unless you specify some other register. Same goes for DS and SI. It's a good thing to keep in mind sometimes. ;)

The first four registers can be split in two parts each.

AX - AH	- The high byte of AX
     AL - The low byte of AX

BX - BH - The high byte of BX
     BL - The low byte of BX

CX - CH - The high byte of CX
     CL - The low byte of CX

DX - DH - The high byte of DX
     DL - The low byte of DX

If you have to put byte into a register to store it for later, you're probably better off putting that byte into one of the high or low bytes of a register rather than the whole register. It'll save some of the precious registers for something else. But note that putting a value into AX, even if its only 1 byte, will clear both AH and AL. Putting something into AL will affect AX but not AH. This goes for BX, CX, and DX as well.

So lets learn some opcodes! We'll start out with the widely used MOV command. MOV puts a value into a another register, memory location, etc. The syntax for MOV is simple:

MOV destination, source

Destination is where the source is being put. Note that MOV doesn't MOVE a value but it copies the value. So something like

MOV AX, 9

will move 9 into AX. If you add an 'h' onto the end of the 9 it will be 9 in hex.

MOV AX, CX

That will copy the contents of CX into AX. Simple. Now lets get into simple math. To add things in asm you use ADD. To subtract use SUB. Bet you didn't see that one coming did you? =).

ADD destination, source
SUB destination, source

destination is where the result of the operation is going to go. Source is what is being added/subtracted. Some examples

ADD BX, 9	;Adds 9 with the value of BX
ADD CX, DX	;Adds DX with CX
SUB AX, 32h	;Subtracts 32 (hex) from AX
SUB CL,	2	;Subtracts 2 from CL

Easy right? Note: A ';' is a comment in asm. Now how about divisions and multiplications? Well just before I show you those, there is something you should know. Multiplying and dividing is SLOW! There is a somewhat limited way around this slowness but I'll show you it later.

MUL source
DIV source

Notice something? They only take one operand. This is because the result of the operation goes into AX. So whatever you pass will be multiplied/divided with AX. Also the source can't be a direct number. It must be a register or memory position. I can only guess as to the reasons for this...

MOV AX, 2	;Moves 2 into AX
MOV DX, 4	;Moves 4 into DX
MUL DX 	        ;Multiplies AX by BX

After this AX will equal 8.

MOV AX, 5	;Moves 5 into AX
MOV DX,	2	;Moves 2 into DX
DIV DX		;Divides AX with DX

Afterwords AX will be 2 and DX will be 1 (the remainder). DX will always be the remainder if there is any.

Here's a tip. Instead of doing ADD AX, 1 you can do the much quicker INC AX. INC is a command that increases the passed operand by 1. The operand must be either a register or a memory location. Also to decrease by 1 faster use DEC. So to decrease BX by one use DEC BX. It's quicker than adding 1 too.

Well that's it for this month! Next month I'll explain more about addressing memory and we'll learn how to assemble and link your asm code to QB!

This article was written by: Fling-master - http://www.qbrpgs.com

Programming SVGA graphics is a good thing to know how to do. SVGA of course allows you to use resolutions of up to 1600x1200 with 32-bit colour depth. With it you can make your games look really really colourful and I find it easier to implement such things as translucency. Of course when SVGA was first introduced, many different video cards started coming out to supoort SVGA modes. The problem with this was that there was no standard way to communicate with the video cards; each one had it's own way of interfacing with it. This was a major problem. A program usign SVGA would have to contain a whole slew of routines to do the same thing on different video cards. So VESA was introduced.

VESA stands for Video Electronics Standards Association. It was made to introduce a standard method to program SVGA. Now instead of programming different routines for different video cards, you should simply use the VGA BIOS extension which handles the work of interfacing with your video card.

Before you get to far into be aware that while you can do SVGA programming in straight QB it won't be fast enough for games and such. If you want speed you should program your graphics routines in assembly.

So lets start. Before using SVGA you must be sure it exists on the computer. We can do this by calling VESA function &H4F00 (Note that all VESA functions are prefaced with &H4F. So &H4F00 is function 0). We can get info on the SVGA card installed by creating a TYPE and passing the segment and offset of it to the function as well.

TYPE VGAInfoType
 VESASignature AS STRING * 4 
 VESAVersion AS INTEGER 
 OEMStringPTR AS LONG 
 Capabilities AS STRING * 4 
 VideoModePTR AS LONG 
 TotalMemory AS INTEGER 
 Reserved AS STRING * 236 
END TYPE 

VESASignature always equals 'VESA' if there is a VESA card. VESAVersion returns the VESA version. OEMStringPTR is a pointer to a string that can be used to identify the different things. Capabilities describes the capabilities of the video card. VideoModePTR is a pointer to a list of video cards supported screen modes. TotalMemory the total memory which the video card contains in 64 kb blocks. Reserved is reserved for future use heheh.

So to detect a VESA compatable card we could do this in QB:

DIM VGAInfo AS VGAInfoType
DIM regs AS regTypeX
regs.AX = &H4F00
regs.ES = VARSEG(VGAInfo) 
regs.DI = VARPTR(VGAInfo) 
CALL InterruptX (&H10, regs, regs)
IF regs.AX = &H4F THEN
 PRINT "VESA found."
ELSE
 PRINT "VESA not found."
END IF

As you can see if a VESA card is found AX will contain &H4F. At the beginning of your program you would need a '$INCLUDE: 'QB.BI' and you would need to load in the QB.QLB library since we're making use of InterruptX.

So that's pretty simple. Now what about plotting a pixel? Well first we need to set a screen mode.

VESA 	Mode Resolution          Colors 
100h 	640x400		 256 
101h 	640x480		 256 
102h 	800x600		 16 
103h 	800x600		 256 
104h 	1024x768	 16 
105h 	1024x768	 256 
106h 	1280x1024	 16 
107h 	1280x1024	 256 
108h 	80x60		 text 
109h 	132x25		 text 
10Ah 	132x43		 text 
10Bh 	132x50		 text 
10Ch 	132x60		 text 
10Dh 	320x200		 32k 
10Eh 	320x200		 64k 
10Fh 	320x200		 16.8m 
110h 	640x480		 32k 
111h 	640x480		 64k 
112h 	640x480		 16.8m 
113h 	800x600		 32k 
114h 	800x600		 64k 
115h 	800x600		 16.8m 
116h 	1024x768	 32k 
117h 	1024x768	 64k 
118h 	1024x768	 16.8m 
119h 	1280x1024	 32k 
11Ah 	1280x1024	 64k 
11Bh 	1280x1024	 16.8m

Those are the available screen modes. So to set a mode we call VESA function 02h. BX will contain the mode number and AX will return &H4F if the mode was set successfully. We also can get info on the screen mode through this TYPE:

TYPE ModeInfoType 
 ModeAttributes AS INTEGER 
 WinAAttributes AS STRING * 1 
 WinBAttributes AS STRING * 1 
 WinGranularity AS INTEGER 
 WinSize AS INTEGER 
 WinASegment AS INTEGER 
 WinBSegment AS INTEGER 
 WinFuncPointer AS LONG 
 BytesPerScanLine AS INTEGER 
 XResolution AS INTEGER 
 YResolution AS INTEGER 
 XCharSize AS STRING * 1 
 YCharSize AS STRING * 1 
 NumberOfPlanes AS STRING * 1 
 BitsPerPixel AS STRING * 1 
 NumberOfBanks AS STRING * 1 
 MemoryModel AS STRING * 1 
 BankSize AS STRING * 1 
 NumberOfImagePages AS STRING * 1 
 SizeOfBank AS STRING * 1 
 RedMaskSize AS STRING * 1 
 RedFieldPosition AS STRING * 1 
 GreenMaskSize AS STRING * 1 
 GreenFieldPosition AS STRING * 1 
 BlueMaskSize AS STRING * 1 
 BlueFieldPosition AS STRING * 1 
 RsvdMaskSize AS STRING * 1 
 RsvdFieldPosition AS STRING * 1 
 DirectColorModeInfo AS STRING * 1 
 Reserved AS STRING * 216 
END TYPE 

I'm not going to explain all these now. Most are self explainatory like XResolutions, and BitsPerPixel. Two of these you should know are WinGranularity and WinASegment. WinGranularity is the size of the memory banks which is important when plotting a pixel. WinASegment is the memory segment which the video memory is located at. This is usually A000h. We can retrieve this information using VESA function 01h. In CX we store the mode number we want info on. In ES:DI we store the segment and offset of the above TYPE. So we could do this to set a mode and get info on it:

'Retrieve mode information
regs.AX = &H4F01
regs.CX = (set-this-to-one-of-the-mode-numbers-listed-above)
regs.ES = VARSEG(ModeInfo)	'Set ES:DI to memory position of the mode info type
regs.DI = VARPTR(ModeInfo)
CALL INTERRUPTX(&H10, regs, regs)

'See if the video mode is supported
IF ModeInfo.ModeAttributes AND 1 = 0 THEN
 PRINT "Unsupported video mode."
 END
END IF

'Now set the video mode
regs.AX = &H4F02
regs.BX = (set-this-to-one-of-the-mode-numbers-listed-above)
CALL INTERRUPTX(&H10, regs, regs)
IF regs.AX <> &H4F THEN
 PRINT "Couldn't set video mode."
 END
END IF

It would be wise to put it into a function that would return True or False based on the success of setting the mode. Or you could return the number of pages available or the video memory segment, etc.

Well next time we'll learn more about memory banks and how to plot a pixel in 256 colour modes and direct colour modes. Stay tuned!

This article was written by: Fling-master - http://www.qbrpgs.com

Necessity or Accessory?

In the commercial world of gaming, much emphasis has been placed on the accessorial parts of the game so that the game can appeal and sell to the mass public. However, in the QB world of gaming, things are rather different. We have a limited number of programmers working on projects, and since most of us (if not all) take up programming QB as a hobby, we don't have much time to write a program at all. Thus, it is necessary that when we write a program, we place the emphasis on the necessities of the programs itself, and not on the accessorial portions of the program, so that the game itself is substantial and not like candy floss just before it becomes wet. Of course, there would different necessities in different gaming genres, so let's begin the analysis now.

1) Ease of play (whether you can play the game or not) This is an absolute necessity across all genres of game play, more so in the QB scene. If you do not know how to play the game, you are not going to play the game. Simple as that. If your QB game is a very simple one, a simple help file that tells you the controls and some miscellaneous things should be enough. However, if your
game is difficult to understand, or there are multiple things you can do in the game, you may need some helpful tutorial levels, or at least some in game help things. If it is an RPG, you can have some characters explaining the more difficult concepts in the game. If it is a First Person Shooter, you might want to throw in a tutorial level or two to let the user be used to how you play the game. If it is something like SimCity, you can allow the user to set the difficulty (or speed) of the game so that the user does not become overly frustrated when he starts playing the game. If all these things still result in people complaining that the game is unplayable, maybe you should change the program. Do you micromanage everything? Let's say you have a Warcraft-like strategy game. If every unit and building has its' own maintenance system and its' own skills, the game would be too tedious to play. Another factor that can result in the "unplayability" of a game is the programmer himself. If the programmer is anexpert in platform games, and he gives fiendish obstacle courses, no one but himself is ever going to cross the game. Therefore, no one but himself is going to play the game.

2) Graphics (resolution + quality of graphics) This one is a very subjective topic. If you asked me this question five years ago, I would have said that graphics leans towards the accessorial portion of game play. Now however, with faster computers and graphics cards and whatnot, people are demanding more pleasing graphical effects for themselves in commercial games. Although QB games are generally not commercial games, we can see more QB programmers putting in more effort in graphics in their games (or even having external graphic designers as part of their programming team) as the years past. Right now in the QB community, my opinion is that graphics are a necessity in most genres, and more of an accessory in a few other genres. What are these genres where graphics are a necessity? In FPS, you cannot deny the importance of graphics as well as the graphical effects. High resolution and fluid movements are the top priorities in any FPS you see. Graphics are a necessity in other kinds of Arcade games also. In space shooters, games such as Dynamic: The Colonization of Jupiter use the Future Library for the SVGA resolution as well as the graphical functions available. Also, Sasha has taken the graphics as one of the priorities as well, so we can see that graphics are more of a necessity. You would not want to play a shooter that has jerky movements together with jarring graphics because it is, to say the truth, unplayable. However, in RPGs and platform games, graphics are not that important. I don't deny that they are needed, by fewer people would mind if you decreased the quality of graphics in RPGs and platform games than if you decreased the quality of graphics in, say, arcade games. That is the reason why I chose to program The Heart Machine and Crystal, both platform games that only uses ASCII characters as graphics. This places the emphasis on other more important factors such as the fun factor and the strategy factor. If you positively think that you need better graphics in your program, you can post a message in any busy QB discussion board, requesting for graphic designers. Someone would volunteer. Trust me.

3) Sound / Music (the thing that comes out from your speakers) Compared to the other four considerations I listed here, sound effects and music is more accessorial in nature. In almost every genre of gaming, you can live without sound effects or music. Think about it this way: if you screw up the sound or music, more people are going to avoid your game than if you don't have sound, or have a mute feature. But not many people are going to play your game just because they heard that your game has great music and/or sound effects. So, if you are not sure whether your music or sound effects are good, chances are that they are not, and therefore be sure to include a feature that will allow the people to mute the sound. Worse, if the sound that needs to be produced is incompatible with the person's hardware or software, you may lose out on a significant portion of the market. (Think about Master Creating's Shadow of Power and the problem it had with onboard sound cards. Even with the new patch there are multiple problems with bosses and with talking to people) The only few occasions when sound is truly essential are in FPSs or in those "educational games" where someone says a word and you are required to type the spelling of the word down =).

4) Storyline (Once upon a time...) The value of storyline varies widely from different genres. This is the reason that explains why there was so much controversy over VPlanet's 35 point rating system, particularly with the part about the 5 points in "Story". For RPGs, the storyline is the lifeline of the game, as the game requires you to play a role in a story. Without the story, you don't even have a game to play with. The storyline is an absolute necessity in an RPG. However, you don't really need a storyline in arcade games or platform games, other than the silly addition of "you have to do so and so to save the world". Having a storyline in these two genres of games is acceptable, but not many people will notice it, as it is more of an accessory. The silliest genre to have a storyline in is the puzzle game. Imagine playing Connect 4 with the computer, and when you lose, you lose a portion of the amount of money you own. Adding a storyline to a puzzle is tantamount to adding a comedy in a World War 2 horror movie; it spoils the whole effect of the game. It is worse than an accessory; it is more like a curse. So, whatever you do, try not to add too vivid storylines to puzzle games. You'll be laughed at. If you cannot think of storylines, ask people for storylines. More often than enough, people are willing to come up with reasonable storylines just for your game. Maybe you can just put those "continue the story" posts in a discussion forum, and watch how the story grows after a day.

5) Fun + Replayability (Why would I want to play the game?) Finally, we come to the fun and replayability factor of a game. I lumped these two factors together because they are related: both contributes to the ultimate satisfaction of playing a game. Needless to say, both of these factors are necessities for successful games, regardless of genres. The definition of fun may be different with different genres though. As an illustration, compare the cutesy fun of Puzzle Bubble with the macabre fun of some violent bloody FPS. The reason of replayability, meanwhile, is the same: to attain perfection. Whether is it to obtain a high score, or to discover the secret formula of opening a secret level, we replay a game because we want really complete the game for what it is worth. And what makes us willing to complete a game is the fun it brings every time we play the game. Sadly, fun is a very subjective thing. What can be fun to someone is totally dead to someone else. There's no real way to determine "funness", except maybe to post demo and ask people for their opinions. This is about the same with replayability. It's just like the ease of play, where some of you find something easy,. but some of you find something impossible.

In conclusion, in the QB community, if you want to make a game that people play, you must make sure you hit the necessities. The accessories are a bonus, but only include them if you have the necessities.

This article was written by: Singsong - http://qbtalk.zext.net

And another issue comes to a close! Hope you enjoyed this issue and all the articles contained within. Next issue We'll have part 4 of the RPG creation series which will cover advanced scripting. Also we'll have part 3 of the EMS article hopefully, another rant as always, and part 2 to the asm series and SVGA series (which won't be written in a rush as they were for this issue).

Until next time!

All site content is © Copyright 2001, HyperRealistic Games. This excludes content submitted by other people.