Implementing Line of Sight in Qbasic Games

Written by Torahteen

Introduction:

Line of Sight is often overlooked in a lot of good QBasic games. This is surprising to me, because it is both easy to implement, and it adds a good touch of realism. But there are numerous ways to put in line of sight. In this tutorial, I'm going to look at two. The circular and diamond line of sight. By the way, these work best in a tile-based game. You'll have to find some other way if you're doing a FPS, in which case these are unnecisarry. Enough ranting, let's get started.

Diamond Line of Sight

Okay, first of all, I don't know what the heck you call either of these methods. I made up the names, so if you find another name for them, it's probably correct. Anyway, the first thing we're going to implement is a diamond view system. In a diamond view system, the unit can see all of everything in a sort of diamond shape around the unit. There are two different diamonds you can do. One of them, has the points of the diamond pointing up, down, left, and right relative to the screen. The other has the points pointing in those directions relative to the unit. I'll be going over the first one, although I will give a short theory as to how you can do the other one.

Since I like to make my games modular, let's make the code into a function. We'll have the game engine call the function, passing in a map, the x and y values, the distance the unit can see, and a reference to the array we should put the final coordinates in. What I mean by final coordinates is a list of points defining what squares the unit can see. So here is our function declaration (BTW, it is not a true function, but a sub):

DECLARE SUB GetLos(map() AS MapType,x AS INTEGER, y AS INTEGER, sight AS INTEGER, result() AS PointType)

MapType should be defined somewhere in the game. But as an example, here is the MapType we'll use in this game.
TYPE MapType
    mType AS BYTE
    Clip AS BYTE
END TYPE

mType is the type of tile (Water, Grass, Rock), and is just an example. We'll be using the Clip status to check if a tile can be seen.

The way I will do all this, is with three nested FOR loops. The first loops iterates from the top of the diamond to the bottom. The second loop increments the width of the diamond by two for every line. The third loop goes through each square in the row. Every loop, we will check each square across the line, making sure that its clip value is false. Here is the code.


SUB GetLos(map() AS MapType, x AS INTEGER, y AS INTEGER, sight AS INTEGER, result() AS PointType))
    Dim nx, ny As Integer
    Dim w, d As Integer
    Dim n As Integer

    n = 0
    w = 1

    n = 1
    redim preserve unitMove(n) as PointType
    unitMove(n).x = x
    unitMove(n).y = y

    For ny = sight To -(sight) Step -1
        For w = 1 To 2 * sight Step 2
            For nx = -(w - x) To (w-x)
                'Checks go here.
                sx = nx
                sy = y + ny
                if sx > -1 and sx < mapWidth and sy > -1 and sy < mapHeight then
                    n = n + 1
                    Redim Preserve result(n) As PointType
                    unitMove(n).x = sx
                    unitMove(n).y = sy
                end if
            Next nx
        Next w
    Next ny
End Sub

Now you're probably thinking "What the heck does this do?!". Ok, notice the three FOR loops. The first two loops basically define the shape of the diamond. Look at the following diagram.

The square in the center is our unit. See the diamond shape around him? If he were on completly clear ground, that is what his line of sight would be. What the above code does, is check each square from the top to the bottom to see if the unit is capable of seeing it or not. Of course, you could see the first part of the mountain range, but not past that. That is not part of this tutorial.

So by starting at the top, w (width) equals one. But for each line going down, it increases by one. That is, until it gets to the unit, at which point it begins decreasing. Then for each line, starting from the left side of the line, we check each square. Simple ain't it?

That is all there is to the Diamond Style line of sight. The biggest draw back of this method, is that it is unrealistic. But it works well for many different games. The next part of this tutorial discusses a more realistic version. Remember that you can also use this to make a diamond movement style. Just pass in the right parameters, and put the correct checks in there.

But what if you want the point of the diamond to always be facing the same direction as the unit is facing? Well, that one is a bit tougher, and I haven't implemented it myself. But I do have a theory. I say that you can divide the diamond into two triangles, then interpolate each one. For each position, just find out what square that is in, then add it. You can then delete the duplicat points. In theory, it should work, but it's up to you to implement it.

Circular Line of Sight

Sure, diamond is simple, and it adds a nice touch to the game. But it is unrealistic. But there is another method. Circular line of sight allows your unit to see square that mark a circle around him. It's a lot more realistic, and it is still quite simple to implement. I hope you remember some of your trig.

Okay. To do this method, we're going to use a simplified ray tracer. Ray tracing is the process of drawing imaginary lines in an outward directiong from the unit, stop when you reach an obsticle. Sound simple? It is!

What we will do, is draw imaginary lines outward from the starting square(the unit), stopping once we hit an obsticle such as a wall, or another unit. We'll draw a total of 360 of these imaginary lines. But first, another diagram...

Sorry, that diagram makes it look not-so-simple. But don't worry, it is. Alright, so what will do is from 1 TO 360, we'll draw an imaginary line from the unit to the outside of the circle. Look at the diagram. You start from the center of the unit, and in the direction theta, incrementing by one each loop. Then you find the x and y of that position, round it down to the nearest whole number, and viola, you have the x and y position. Then you check that square to see if it is a building, wall, or anything of the like. If it is, then add the square, then exit the loop, going to the next angle.

So how much do we increment it, and how do we find the x and y position? I'll answer the first question first. Remember that QBasic/FreeBasic's built in trig functions can not directly take degree measure. Instead, you must use radian measure to measure your angles. Since we need to cast a ray once every degree, we must divide the number of radians in a circle by 360. From trig, we remember that there are 2PI radians in a circle. If we declare a constant at the beginning of our program called PI, then PI = 3.1419526. The increment is then 2*PI/360.

To find the x and y positions, we just use some more of our trigonometry. Look at the example. According to what we learned in trig, SIN(theta) = y/r, and COS(theta) = x/r. We know theta and r, so:

Since we need whole numbers, not floating point, we just cast these into integers. Here is our code:

Sub GetLos(map() As MapType, x As Integer, y As Integer, sight As Integer, ByRef result() As PointType)
    Dim theta As Double
    Dim inc As Double   'Increment amount
    Dim quit As Byte
    Dim n As Integer

    n = 0               'Just in case
    inc = (2*PI)/360
    theta = 0           'Just in case

    For theta = 0 To (2*PI) Step inc '360 times
        For i = 1 To sight           'Drawing our imaginary line
            'Find the position
            nx = Int(r * COS(theta))
            ny = Int(r * SIN(theta))
            'Check the boundries
            If (nx < 0 Or nx > mapWidth Or ny < 0 Or ny > mapHeight) Then
                Exit For
            End If

            'Check for clipping
            If map(nx, ny).clip = TRUE Then
                quit = True
            End If

            'Add the square
            n = n + 1

            Redim Preserve result(n) As PointType

            unitSight(n).x = nx
            unitSight(n).y = ny

            If quit = True Then
                quit = False
                Exit For
            End If
        Next i
    Next theta
End Sub

That is pretty much it. Only thing is to get rid of doubles. We just add this at the end.

Dim IsDup As Byte
    Redim tempList(0) as PointType

    For i = 1 To uBound(result)
        IsDup = 0
        For d = 1 To uBound(tempList)
            If result(i).x = tempList(d).x AND result(i).y = tempList(d).y Then
                IsDup = 1
            End If
        Next d

        If IsDup <> 1 Then
            Redim Preserve tempList(uBound(tempList)+1) As PointType
            tempList(uBound(tempList)).x = result(i).x
            tempList(uBound(tempList)).y = result(i).y
        End If
    next

    Redim result(uBound(tempList)) As PointType
    For d = 1 To uBound(tempList)
        result(d).x = tempList(d).x
        result(d).y = tempList(d).y
    Next i

See how simple it all is? And to think that you don't see it in more QB games.

Conclusion

Ok, so now you know how to implement line of sight into your games. Now you just need to go implement it! I hope you had fun reading my tutorial, it wasn't half bad writing it. Until next time...

Torahteen