Issue #4 Saturday, October 6th, 2001 |
Credits Editor: Writers/Contributors: |
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!
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
|
|
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. |
The results from this issues voting will go in here next issue =) |
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!
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.
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...)
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
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
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
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
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
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.
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.
Send a 0 to the DMA clear register to stop any current transfers.
Set the DMA transfer mode to output by adding 48h to the DMA channel number and sending it to the DMA mode register.
Set the address and page.
Set the length to transfer.
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
Tell the DSP that we want to set the digitized sound transfer time constant by sending 40h.
Send the time constant: 256 - 1,000,000 \ Frequency
Tell the DSP that we want to output a sound using 8-bit single-cycle DMA by sending 14h.
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
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
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
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. |