Writing better code: The self taught coder

He’s very kind. Really it’s a chaotic and buggy thing that doesn’t compare to modern roguelikes at all. It was one of the first that tried to present a sort of a world instead of just a random dungeon, but that’s about it, really.

Nah, I never would have figured that out on my own! I doubt I would have been able to recreate the precise darkness positions on my own; I would have either gone with some kind of roguelike raycasting solution or driven myself mad.

So I am now writing a little ‘line-of-sight crawler’ script that scooches down the diagonals until it hits either an obstruction or a cardinal direction. If it hits the obstruction it renders the source tile black; if it hits a cardinal it scooches down that avenue toward the player, again rendering black if it encounters an obstruction on the way.

I’m constraining it just to the NW quadrant for now, and slowly enlarging the number of vertical slices affected… bugs keep popping up and I have to backtrack and simplify before I can squash them. I’ve learned to move forward at a rather gingerly pace.

Thanks again to @jsnell for digging that up; I googled the hell out of ‘Ultima III line of sight’ and never came across that page.

This screenshot shows my algorithm in action. The red ‘sparks’ are instances of the crawler ‘trying’ to reach the player, petering out if they hit a mountain or forest first. (I’ve also temporarily put borders around the grass and water tiles to make them easier to count.) When I feel it’s working I’ll compare screenshots between the real Sosaria in Ultima III and my ‘fauxsaria’ and check for any discrepancies.

los-crawler

Fingers crossed. This has been a bear and I would really like to be able to move on, but I don’t want to do more world building until I’m confident the LOS issue is solved, since knowing there are pockets of darkness will heavily impact said world building. Next I’ll go back to inventory and NPC systems, leaving combat for now as a ‘black box’ to be solved later.

It’s looking good so far!

since knowing there are pockets of darkness will heavily impact said world building

Do you plan to alter the world from stock U3?

Oh yeah, the U3 map is just to test the line of sight. I’m drawing a different map altogether for the actual game.

I think I’ve finally reached zero discrepancies between Sosaria and fauxsaria!

I ended up writing 4 separate darkness scripts, one for each quadrant around the player. The code could probably be more elegant but at least it works.

This rather radical darkness algorithm probably looks better with the limited viewport of Ultima III than it does with my larger view. I may try the Ultima IV refinement later, or try to execute a roguelike-style raycasting system.

But for now, I’m gonna declare victory and move on to other systems. What a relief.

Argh, it causes massive slowdown in my town map (oddly not in my overworld map). Will have to see if I can optimize.

I think I was able to improve it a bit. Gamemaker has two events, the ‘step event’ which updates the game state every X times per second, and the ‘draw event’ which renders onscreen items after the step event has completed. Previously I had all of the line-of-sight logic being calculated in the ‘draw event,’ but Gamemaker documentation/forums indicate this is inefficient, that all serious calculation should be done in the step event.

So, I took the line-of-sight logic and put it in the step event, such that the game first figures out which tiles should be black, then puts those tiles into a ‘grid’ data structure. The draw event then reads the grid and draws black on the appropriate tiles.

The game feels less sluggish now, although still not quite as smooth as I might like it to be. I imagine the step event is still doing a lot of unnecessary work, as it currently has to calculate the line of sight six times per second instead of just once per player move.

I tried making the logic trigger off player movement keypresses, instead of the step event, but that caused bugs of its own, with the darkness blocks being offset in weird ways. Might tinker with that more later. I can’t help wondering, if I’m already getting sluggishness at this point, in a world largely unpopulated with objects and NPCs, It could be a real issue later on. Once again in awe of what the Old Godz were able to do with a tiny fraction of the CPU/memory resources we now take for granted.

Updating the grid on each player key press is definitely the correct direction to go.

A) are you triggering the grid update after all of your ‘move the player’ code?

B) do you know how threads/synchronisation work in gamemaker? Are the update event, draw event and keyboard input even all synchronised properly? If they aren’t taking possible there’s a data race between the drawing event and the input event, with you updating the grid in the middle of the drawing code reading it?

Hey. Do you think you could post a snippet of your script? I’d be interested in seeing your solution.

Also, I’ve been slowly plugging along on a personal project in GameMaker as well? Mind if I post here?

Please post away!

Most of my line of sight logic is in this ‘crawler’ script. There are four variants, one for each quadrant of the screen, with x/y modifiers flipped as needed. Below is the ‘northwest’ variant, meaning it starts with the northwest-most tile on the screen (player.x-320,player.y-256) and starts crawling diagonally toward the player until it hits an obstruction or an X or Y axis. I’ve commented out some of the earlier code, where it would draw red sparks on each tile that was ‘crawled over’ en route to the player, and where it would directly draw black on the obstructed tiles. Now it puts the location of the tiles into a ds_grid which has the same dimensions as the player view. If it calculates that a given tile should be black, it puts a ‘1’ into those coordinates in the ds_grid (offsetting to account for the difference between 0,0 and 'player.x-320,player.y-256, and dividing by 32). The draw event will iterate through the grid and look for ‘1s’ then draw a black square at those spots.

//this iterates the X value of the tiles that send out crawlers
for(p=320;p>=0;p-=32)
{
//this iterates the Y value of the tiles that sends out crawlers
for (n=256;n>=0;n-=32)
{
//this diagonally iterates the location of the crawler tiles
for (j=32;j<352;j+=32)
    {
    //this draws sparks on the crawler tiles
    //draw_sprite(spr_redSpark,-1,obj_guy2.x-p+j,obj_guy2.y-n+j);
    
    //crawler intersects X axis    
    if(obj_guy2.y-n+j>=obj_guy2.y)
        {
        //scooch east
        for(l=0;l<352;l+=32)
            {
            //draw_sprite(spr_redSpark,-1,obj_guy2.x-p+j+l,obj_guy2.y);
            if((position_meeting(obj_guy2.x-p+j+l,obj_guy2.y,obj_opaqueObstruction)) ||
            (position_meeting(obj_guy2.x-p+j+l,obj_guy2.y,obj_opaqueTraversable))) &&
            //exclude case where player is on an obstruction tile
            (j+l-p!=0)&&
            //prevent darkening of adjacent tiles
            (point_distance(obj_guy2.x-p,obj_guy2.y-n, obj_guy2.x,obj_guy2.y)>48)
                {
                //draw_sprite(spr_blackness,-1,obj_guy2.x-p,obj_guy2.y-n);
                ds_grid_set(global.blackTiles, (320-p)/32,(256-n)/32,1);
                //ds_list_add(global.nwXList,obj_guy2.x-p)
                //ds_list_add(global.nwYList,obj_guy2.y-n)
                break;
                }
            //crawler intersects player    
            else if(j+l-p>=0)
                {
                break;
                }
            }
        break;
        }
        
     //crawler intersects Y axis    
    else if(obj_guy2.x-p+j>=obj_guy2.x)
        {
        //scooch south
        for(l=0;l<352;l+=32)
            {
            //draw_sprite(spr_redSpark,-1,obj_guy2.x,obj_guy2.y-n+j+l);
            if((position_meeting(obj_guy2.x,obj_guy2.y-n+j+l,obj_opaqueObstruction)) ||
            (position_meeting(obj_guy2.x,obj_guy2.y-n+j+l,obj_opaqueTraversable))) &&
            //exclude case where player character is on an obstruction tile
            (obj_guy2.y-n+j+l!=obj_guy2.y)
                {
                //draw_sprite(spr_blackness,-1,obj_guy2.x-p,obj_guy2.y-n);
                ds_grid_set(global.blackTiles, (320-p)/32,(256-n)/32,1);
                //ds_list_add(global.nwXList,obj_guy2.x-p)
                //ds_list_add(global.nwYList,obj_guy2.y-n)
                break;
                }
            //crawler intersects player    
            else if(obj_guy2.y-n+j+l>=obj_guy2.y)
                {
                break;
                }
            }
        break;
        }   
        
    //crawler hits an obstruction
    else if((position_meeting(obj_guy2.x-p+j,obj_guy2.y-n+j,obj_opaqueObstruction)) || 
    (position_meeting(obj_guy2.x-p+j,obj_guy2.y-n+j,obj_opaqueTraversable))) &&
    //exclude case where player character is on an obstruction tile
    (!position_meeting(obj_guy2.x-p+j,obj_guy2.y-n+j,obj_guy2))
        {
        //draw_sprite(spr_blackness,-1,obj_guy2.x-p,obj_guy2.y-n);
        ds_grid_set(global.blackTiles, (320-p)/32,(256-n)/32,1);
        //ds_list_add(global.nwXList,obj_guy2.x-p)
        //ds_list_add(global.nwYList,obj_guy2.y-n)
        break;
        }    
        
    }
}
}

That’s all happening in the Step Event, though as noted above I may change that. At the beginning of each step it clears out the grid, filling it with 0s:

ds_grid_clear(global.blackTiles,0);

Another object called ‘drawDark’ has a small bit of code in the Draw Event which iterates through every place in the ds_grid looking for ‘1s’, converts those locations back to coordinates relative to the player, then draws the black squares (the keyboard check at the top is just a capability I put in to remove all the darkness by holding down the right control key, which obviously wouldn’t be retained in an actual game):

if(keyboard_check(vk_rcontrol)==false)
for (i=0;i<=20;i+=1)
    {
    for (j=0;j<=15;j+=1)
        {
        if(global.blackTiles[# i,j]==1)
            draw_sprite(spr_blackness,-1, obj_guy2.x+(i*32-320),obj_guy2.y+(j*32-256))
        }
    }

I hope my code isn’t too spaghetti-esque, but I’m a raging newbie so it probably is.

Good questions. There’s probably garbling going on because different objects are reacting to the player keypress events in different ways. What I would like to do is have all world events key off the turn counters, which in turn is keyed off player actions. But I haven’t yet figured out yet how to have a variable change trigger other stuff. So I’ve been putting that off…

I think the mods should fork this thread into a new one! Because of the old necro with a weird title people might not be seeing this thread? (i.e. it wouldn’t appear in the ‘new’ category).

You can probably “fire” your own events in Gamemaker, so you could just have a single “update the universe” event and do everything in there?

I don’t know GameMaker, but would bet that this is the problem. This reads to me like a collision check of the square you’re currently processing against all “opaqueObstruction” objects on the map by looping through the list of all objects. The more such objects you have on the map, the slower each individual check becomes. Unless there’s a spatial index somewhere behind the scenes.

Typically in a tile based game the map would be presented as a 2d array (or several). To find out what’s contained at the given map coordinates, you’d just look up the map coordinates in the appropriate array.

this code is hard to follow, need some spaces separating parameters and other formating

also some values are hardcoded, making them mysterious, like 320 or 32. easier to read if where screen_width or something like that

good code is easy to read

Should I declare relevant variables at the top of the function, like

screen_width=320
tile_size=32

etc.?

I don’t know this programming language, so I don’t know if thats the right place.

But yea. You want to write code that is easy to read, and using variables with a name that give hints about the content is a good way to make your code easier to read.

while you are declaring your variables is a good place to write a description of these variables or maybe some caveat

// Screen width on uniform devices with HXCAN hintilines. Please don’t use column “0” that is reserved
screen_width=320

// Tile size for podrons using elevated horses with canoes. must be power of 2.
tile_size=32

A good side effect is if you want to change what these constants means, is easier to change in one place than in many.
But thats the least important feature, the most important is to make your code easier to read for other programmers or yourself in a month or two when you have forgot why you wrote your code like that.

I’m not even going to try to understand your code, and the terminology you’re using is… odd. So I’ll loosely describe how I’d do this.

Initialize everything to not visible.

Populate a list (array) of target cells to process, consisting of the top and bottom screen rows, and left and right screen columns.

For every cell in the list, cast a ray (using the Bresenham line algorithm) from the player position to the cell. Mark every cell it hits on the way as visible, until it hits an opaque cell or reaches the target cell, at which point stop and move to the next target cell.

If the function that determines whether or not a cell is opaque is sufficiently expensive, some time savings could also be realized by pre-creating a temporary 2D array of Booleans that only stores whether a cell is opaque or not.

As discussed above, I was trying to recreate a specific LOS technique used in Ultima III, and raycasting doesn’t do that. I did try using Gamemaker’s built-in “collision line” function to see if each tile along the border is obstructed along a path from the player (see screenshot below), but when translated into tiles of darkness it ended up making some very peculiar choices that looked ugly. That said I am interested in exploring raycasting further. For now I am satisfied with the ‘Ultima’ approach except wrt optimization, clean code, etc.

Can you please explain what is… odd about my terminology?

Nor did I ask you to. I posted that specifically because GregB wanted to see it.

I would love if the stack overflow 'tude could be avoided in this thread, but whatever.

The problem might have been with drawing the lines between the top left corners of the tiles rather than the middle of the tiles.