Content

NewoZero Postmortem (programming)

Page is part of Articles in which you can submit an article

written by owen on 2021-May-16.

related image
related image
related image
related image
related image
early signs

early signs

AABB everything

AABB everything

Retro sun

Retro sun

Menu UI

Menu UI

Title screen

Title screen

track texturing

track texturing

In game debugging

In game debugging

deep under the city

deep under the city

simple frustum

simple frustum

This is roughly game number 7 in my series of games written for the Wii game console using the C programming language.  The easiest way to play them is if you have a Wii connected to your television - otherwise tough luck - this is a niche hobby.  This game is roughly 5 months of on/off work but overall a result many years tinkering with a little white box - which helps if you want to get into this sorta thing.  So far many people seem to like it.  It has been played by more than 287 people across the world even though it suffered 3 days of leaderboard downtime.  The online leaderboard is the only metric I have available so when it goes down I have no idea if anyone is playing.

I appreciate everyone who helped out in the promotion, testers, debuggers and the 200+ post #gamedev thread on Gbatemp. I will try to cover as much as I can remember in this article but random ramblings are also on twitter @newogame and screenshot blog.

Early Concept

I've always wanted to do a racing game but failed because of my lack of knowledge when it comes to matrix math.  For this game I played it safe and kept my ambitions simple.  At the time I was thinking of making a driving game along the lines of Road Rash, Rad-Racer or FZero - long flowing tracks, traffic (but not the annoying kind that tries to kill you) and 3d backgrounds. Low poly graphics is my typical art style - I get to reuse models I had already created and I can iterate quickly building on the knowledge I learnt from previous games.  It had been 6 months since I updated NewoEscape and 2 years since my failed attempt at a brand new open source game engine called City Demo.

Prior to creating this game I became fascinated with colourised walking videos videos shot at night in cities like Hong Kong and Shibuya.  Added to that the "Miami Nights 1984 - Accelerated" music video blew my mind the first time I saw it.  I was all into the retro synthwave vibe.

"It’s like I have nostalgia... for something that never happened" - Luke

Early 80s computer graphics demos like StarRider (1983) and Marks & Marks (1981) have always been an influence on the work I did - simple while still giving room for innovation.  The level of detail matched perfectly with what I could achieve on the wii hardware without advanced programming knowledge.  Artists like liamwong and the launch of Cyberpunk 2077 fueled my need to create something in a kind of "retro-future" vibe.  There are even many games that go for the "purple blue cyan" vibe, none quite as spectacular as the work by AntonKudin developer of #MegaSphere.

Development

The racing games that currently exist on wiibrew were all very old and not of much help.  The racing genre on wii homebrew did not seem very popular at the time.  They mostly fell in the "timed" or straight line infinite runner genre.  I aimed for winding roads, hills and arcade physics that would require the game to be fully 3d.

As with all of my hobby homebrew projects I have to be very careful with my initial game development goals so that I stay focused on what I "can" build without being distracted by the things I cannot. So the first thing I worked on is getting the car to accelerate,  turn left and right. Once unpowered turning was coded I then started generating a track which is mostly a series of points (4096 wide cubes). Generating this track gave me a bit of trouble at first. I re-wrote the generation that I created for newotokyo because I wanted racing courses with smooth high-speed corners. In the process of re-writing it I made it much simpler (which is what you want to do every time you rewrite your code) - as opposed to more complicated.

The problem with auto generating a track (vs hand creating) is dealing with the overlapping sections. When you hand craft levels you avoid overlaps. Auto generating the track saves me a bunch of time , time which I THEN waste by writing more code to fix bugs in the auto generation. lol. Such is life. Anyway I had originally done some overlapping track code in citydemo but I could not even look at that code anymore because it was so complicated I wrote something simpler.

And you know technically I could build a driving/racing game with just these features; gas, breaking, turning and a track. But of course I like to add a little extra just in case someone "actually" likes the game they have something extra to play when the wii scene is no longer active.

Ever played a game to completion and wished there was just a little bit more?  That' is why AAA games are so popular - they have tonnes of extra and often pointless features. Wii homebrew doesn't have that luxury or time.  I can never reach AAA quality. But I can always tinker around until I accumulate enough features to call it a "GAME".

Development Hurdles

Overlapping track sections resolved, collision response was another big issue.  I was now dealing with a fully 3d space. Going down hill was relatively easy - uphill was a whole other story.  The camera had to be looking ahead of the player to ensure that it was at the right angle so that the player could see where they were going. The track is broken up into cubes of 4096units.  All this without being a jerky mess. I started coding in late Nov2020. Thought I could release something by monthend of Dec2020 but that did not happen.  I barely had a playable build.

The way I code prevents me from easily being discouraged by the many hidden insurmountable tasks that might be ahead of me. Problems just popup, one at a time.  As I started looking at more FZero videos I realised that I needed tilted tracks and the camera needed to consider the up vector. THIS CHANGED EVERYTHING!

And when I say everything. I mean EVERYTHING! Collisions no longer worked because they assume the walls are to the left and right of the track spline. Gravity is no longer pointing down -y.  Turning left or right no longer worked because the usual assumption was that turning was a rotation around the Y axis.  A literal nightmare of research that revealed that it required some camera matrix manipulation.   Worse case I would stick with flat courses.

It took about 2 months to resolve the wall collision and camera rotation issues - even now it is not fully resolved but I settled on slightly tilting the track (which is enough to give the illusion). 

"Trust me when I say this is legitimately a really really really fun racing game. I'd download this if I saw it online!!" - BlazeMasterBM

By Mar2021 I was well on the way to adding NPC traffic. Mid Mar2021 I went full Shibuya at night by rendering letters of my game font on the in game buildings as signs.  This blew the lid off the look and feel of the game.  I was testing the game mostly on dolphin at this point clocking about 30fps on my new computer but when I played it on real hardware I said to myself; "I think this might be my most awesome looking homebrew ever. The thing looks like a dream when running at full speed.".

I stumbled on a MP3 player bug that would crash the game after a certain number of plays (just enough to not notice during testing).  I ended up ripping out all my sound fade and hacking into the threading code, then leaving my wii running for 8 hours straight to ensure stability.  The reason why I have to do a test like this is because I don't want someone to set up a wii arcade box only to realise that the game crashes every 20 minutes. I think of these homebrew games as arcade cabinets running all day. Which means no room for random crashing. The game has to be stable.  This bug was very frustrating to track down since the mp3 player is running in a pthread, beside the main game thread but launched in a separate thread that I create and manage (I need this third thread to prevent the stutter that occurs if you load an mp3 during gameplay).

Generating the city

Most of the city is comprised of flat unlit cubes that fade from purple/red (at the top) to black at the base. The track/road is generated first, then the buildings as placed in and around the world/track center. The signs are then placed on the buildings randomly. If the building is short or wide then it is unlikely to get a sign. The buildings are placed using a grid with odd steps in order to hide the grid like nature. Several grids are overlaid with the most sparse versions being first then more compact ones added later so that the mix (of heights and widths) seem more organic.

Doing collision tests against the track ensures that no buildings are spawned inside the track. While collision tests against each building ensures that they do not spawn into each other. Originally the building-to-building collision testing was in 3d but later realised that this was a waste of time and that only testing the base was enough to get the job done. Almost half of the loading is this process building-to-building collision testing.

typedef struct{
        guVector pos, size;
        u32 col;
        bool active, has_sign, is_visible;
        short type;
} buildStruct;

static buildStruct build_list[512];

Signs of the times

The signs are as simple as they look. While I was coding the buildings I used some placeholder lines to make basic shapes but I knew that eventually I would have to use some form of texture in order to allow others to help create signs I could load from disk. While looking around for place holder sign art I remembered that the in-game large font was infact stored in an image map. So I dug in, picked a random frame from the image map boom - still using the font. The sign png is loaded in the game directory so mod-ers can have at it if they so desire. Fonts are convenient because they stretch well.

There are at most 512 signs depending on the number of buildings and the style that is choosen. Each sign is individual, even when they appear as a group. The animations run on a global 10 step cycle. Each sign is affected differently by this cycle based on its index in the array. For example "odd" signs might go dark on step 2 or change colour on step 7, or add a frame on step 9 etc. Its basic stuff but predictable. Some signs are a combination of index modulous style mod step.

Generating the track/road

The track itself is a series of sections that are randomly chosen using rand(). I really should be using a proper noise function but I let it in to work on other issues. The game has 10 tracks in each of 3 difficulties. The number of tracks is potentially infinite but I settled on 10 so the game would have an ending. I figured that was good number of a modern game. The tracks on EASY mode have faster-highspeed curves and do not rise or fall unless there is an obstruction. Therefore easy mode has no tunnels, only a few bridges because the likelyhood of a overlap is reduced when the corners as longer.

If a overlap does occur the track is raised - ALWAYS - above the other. Overlaps are a big pain but one of my favourite thing about 3d racing games. There is a whole function for correcting overlaps after the track is laid out. Basically every overlap is raised by a fixed amount then I walk the array backwards raising every point until I meet a point which does not need to be raised. I walk the array forward in a similar fashion. I repeat this process 5 times because one function call often creates new overlaps. 5 is good now but in the future I might need a better strategy.

Tunnels are not much of a problem.

The NORMAL and HARD modes have longer tracks. HARD mode has the longest possible track that the engine can render which is 1024 section. Of course I could probably triple the track length but the length affects all sorts of things such as loading times, collision detection, buildings, overlaps, etc. The 10th level of each difficulty is the full length - just because. On EASY - track 1-9 is roughly 45% of the full track - because EASY.

The track fades to black from the position of the player and is recalculated every second. This is done because drawing it unlit does not simulate distance well - I could put some lights in but - time. The track is a series of vectors that is stored in an array. The track width and up vector is used for simple collision detection. The entire track has the same width for simplicity of the AABB. It is possible but difficult to escape from the track. Initially I wanted to have fenceless track sections that allowed for track skips but I quickly scrapped that idea - because physics.

typedef struct{
        guVector pos, norm, r, lp, rp, fp, bp, ln, rn, up, up2;
        u32 col;
        float distance_from_player;
        bool is_cull, is_raised, rb, ra, is_scored,
                        is_trackstart, is_trackend, is_special, is_flagged, is_door, is_tunnel,
                        is_inlist;
        short stype;
} trackStruct;
static trackStruct track_list[1024];

Game Controls

With this game I used wiimote-horizontal controls for 90% of the development time.  I never touched the nunchuck.  In the last couple weeks of beta testing it was @tinyvast who suggested adding motion controls and digital shoulder button acceleration.  I am usually hesitant with adding motion controls to anything. With my wii homebrew I usually stick to simple controls scheme and avoid requiring a particular setup or motion controls (because I find them finicky and imprecise).  But motion controls are a kind of love hate relationship thing on the wii so I put it in as a option (everyone likes options).  It is not hard to code but very hard to get right along with a whole suite of needed fixes which are too many to list.  A few like; Turning filter, Dpad filter (digital easing of dpad input using a timer), motion control sensitivity, digital acceleration, acceleration lag (used for animating the flame).
Here is the game controls code looks;

...
        //turn off motion controls if I press a button on another controller
        if(button_on_classic | button_on_gamecube) game_motion_controls=false;

        accelerate_factor=0.0f;        //variable accel using analog buttons/sticks
        
        if(button_r_analog > 0) //shoulder button
                accelerate_factor=button_r_analog * 1.25f; //1st //80% is full thrust
        if(button_analog_y_right > 0) //analog stick
                accelerate_factor=button_analog_y_right * 1.3f;  //2nd
        if((button_motion_roll > 0) & game_motion_controls) //motion controls roll value
                accelerate_factor=button_motion_roll / 60.0f; //motion //3rd
        if(button_shoot_held) //2, x, A, Z buttons
                accelerate_factor=1.0f; //default all out //4th
        
        is_accelerating=( accelerate_factor > 0 ); //flag I use in most of the code
        
        //turning controls below
...

I have an input controller API that returns a huge list of boolean variables from all the controllers simultaneously connected to the wii player 1 (gamecube, classic, wiimote, wiimote motion and nunchuck).   This allows the code above to be so simple.  Allot of the sticks return values in different ranges but I have mapped them all to stable range of 0-1 or -1 to 1.  For the sake of simplicity.
Also motion controls are not the same as MarioKart;

if(game_motion_controls) button_motion_roll+=40.0f; //we play with wiimote mc at 90 degrees 

Difficulty


During the first open beta test I noticed that 80% of the players were not getting past level 1 on normal mode.  Which is strange because the game is dead easy (once you get the handling down).  Usually I would leave the game on "normal" then let players down/up-grade if they get frustrated.  But this seemed like a special case with SO MANY players not being able to pass the first level which is at most 3 minutes long!  It could be the reason why Ninty doesn't make a new FZero game - racing games seem to be hard for most players.Quick thinking in the devlog we decided to drop the default difficulty to easy. So now the first experience players get with the game involves smooth corners with no tunnels.  It seems to have improved the up take a little but only time will tell.  The problem with this is that all the "good" players have dominated the leaderboards.  Some beating easy mode on their first play through or even setting world records.

Conclusion

Racing games are fun, low stress once you get them working.  NewoZero was designed so with a speed allowing you to never need to release the accel button - most only realise this after several plays.  In the future I might work on AI racers but certainly include an Arcade mode with additional challenges, barriers in the road etc.  For now the coming version 1.1 I will be updating the background details and implementing the new view frustum code that I recently discovered - saving myself much needed fps.

Game Trailer


Things I have learnt / Improved / Fixed

  • There was this long standing bug where the game would get stuck in the fading transition after you selected a menu option.  I never figured it out until I was testing the menu response time and noticed that it was possible to "un-select" the menu option after the screen had started fading.  This resulted in the menu_next_stage=false and the fading_is_done() would no longer be called hence? stuck.  Solution?  Modify the menu UI so that buttons are not "bool value=!value;" but are "bool=true;".  This way no more accidental de-selecting.

    if( (!is_track_start) & (menu_next_stage | menu_restart_option) & game_wait_tick_is_done() ){
            if( fading_is_done() ) game_reset_stage();
            return;
        }

  • The fade transitions were getting really slow because of all the generating I was doing in the background so I came up with a function I could call inside loops and between functions.  It is a crude but effective progress bar.  This function will render a different colour square to indicate to the player that something is happening;

    void fading_pulse(){
            if(fading_percent <= FADING_MIN) return;
            pulse_flip=!pulse_flip;
            pulse_count++;
            engine_2d();
            if(pulse_flip) GRRLIB_Rectangle( engine_scx(), engine_scy(), 10, 10, colour_get_next(), 1 );
            engine_render();        
    }

  • Fixed a bug in the displaylist where it would flash during the first creation.  All I had to do was auto draw the object at the end after creating the display list - problem solved. No flicker. The entire track is being displaylisted every second. Pointless? maybe.
  • The index passed to dl_begin(int index, u32 col); was actually being incorrectly used as an internal index.  Fixing this bug fixed a few allocation problems and now its more difficult for me to accidentally overwrite displaylists now that they are properly indexed by both an int and a u32 colour.  With an internal index that allows 128 unique entries.
  • The menu now uses 2 timers; 1 for the up/down movement delay and another for accepting a selection.  This way quick movements and selections are now possible without missed presses.

    static bool mte_acc(){ //check if we can accept something //accept timer
        if(timer( menu_accept_rate, &menu_accept_time )){
            menu_idle(); //eat the idle timer if the user clicks something
            return true;
        }
        return false;
    }

    bool menu_idle(){ //2021 //quit test to see if I can accept a new click or if I just clicked on something
        return timer( menu_idle_rate, &menu_idle_time );  //idle timer eats itself if you check it
    }


  •  New generic menu function for handling its interactions.  I used to have this bit of code in every game but since it rarely changes I created a generic function.  Accept/Cancel buttons were checking on "held" but since this game has an action button that you hold I have now be changed it to "down";

    bool menu_draw_generic(int list_ind){ //2021
        return menu_draw_simple( list_ind,
            button_accept_down,
                button_cancel_down,
                    (button_down_up | button_down_held | button_down_down),                                 (button_up_up | button_up_held | button_up_down),                                    (button_left_up | button_left_held | button_left_down),
                                                    (button_right_up | button_right_held | button_right_down)    );
    }

Footnotes

  • I had accidentally selected an option to "dump frames" in Dolphin - which resulted in my harddrive filling with avi files - 120 gigs to be exact.  Almost leading to a harddrive crash.  I was going to delete and only keep the best ones but then I thought to myself that I could upload them to youtube. This gave rise to the brand new NewoGame on youtube channel.   Now you can watch silent videos with placeholder graphics as I try to fix various bugs in the game.

  • This looks really cool. I just wish this guy would stop naming all his games "newoword". It strips them of their individuality. Kinda reminds me of how every N64 game had "64" in its title." - reddit


    This is because my games are Art Style games.  Designing new names and branding for every game is hard.  They are all related in some way.


permanent link. Find similar posts in Articles.

comments

    Comment list is empty. You should totally be the first to Post your comments on this article.


comment