💾 Archived View for thrig.me › blog › 2024 › 02 › 04 › state.gmi captured on 2024-06-16 at 12:36:59. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-02-05)

-=-=-=-=-=-=-

Keeping State

One may be mostly done with a game when the realization hits that one might need a title screen and maybe some other not-part-of-the game (a "write a game in seven days challenge" is a bad time to learn these sorts of things). Probably you can bolt on something if-won show-the-win-screen or something like that? A better plan might be to have different states and the means to switch between them. Maybe you learned something about finite state machines that I didn't whilst getting that B.S. in Geology? Anyways, this can simplify the game loop at the cost of shoving everything off elsewhere.

    running = 1;
    while (running) {
        state_render();
        state_update();
        sleep(1);
    }

Prior to the loop you'll need to setup the first state, probably a title screen or main menu. The states should probably be a stack, so you can push a help menu and pop back to whatever was running before, which could be the main menu or game or who knows what. There may be utility in replacing the current state with something else, but push and pop should suffice. There probably should be "on enter" and "on exit" functions that get triggered on state change; these allow allocation of the title screen and cleanup when that goes away. And also "render" and "update" functions where the "render" draws stuff, and the "update" handles getting input from elsewhere, doing physics, and all that business logic. Player input could also be a third call somewhere in the game loop, but that's not much different from doing it somewhere in the "update" function for the state in question.

A handy tip for ncurses is to "touchwin(menu)" right before changing to a different state; this means when the menu state comes back it is flagged to be redrawn by the render call (or probably during the "update" call when a "wgetch(menu)" triggers a refresh.

The state functions will need access to "running" to stop the game loop, though one could instead change to a state that makes the process exit. It may be good to pass in a pointer to all the state functions so they can access some "app" bag of holding, in which case "running" might be in that struct or hash reference or whatever.

A very silly example follows. The important part here is (provided that the state changes are handled correctly) that it is pretty easy to wire up, say, a game of tic-tac-toe or other states as need be. Also the code for the different states can be shoved off into their own files or namespaces, etc.

    #!/usr/bin/env perl
    # state.pl - a silly game state example
    use 5.36.0;
    use constant { UPDATE => 0, RENDER => 1, ENTER => 2, EXIT => 3, };

    my ( $Player_HP, $Troll_HP );

    my $Is_Running = 1;
    my @Game_State;
    state_push( make_state( \&menu_update, \&menu_render ) );
    while ($Is_Running) {
        $Game_State[-1][RENDER]->();
        $Game_State[-1][UPDATE]->();    # no sleep because this blocks
    }
    exit(1);

    sub make_state ( $up, $rend, $enter = undef, $exit = undef ) {
        my $s;
        $s->@[ UPDATE, RENDER, ENTER, EXIT ] = ( $up, $rend, $enter, $exit );
        return $s;
    }

    sub state_push ($new) {
        $new->[ENTER]->() if defined $new->[ENTER];
        push @Game_State, $new;
    }

    sub state_pop {
        $Game_State[-1][EXIT]->() if defined $Game_State[-1][EXIT];
        pop @Game_State;
    }

    sub menu_render {
        say qq{enter "play" or "quit"};
    }

    sub menu_update {
        my $choice = readline;
        if ( $choice =~ m/(?i)p/ ) {
            state_push(
                make_state( \&game_update, \&game_render, \&game_enter ) );
        } elsif ( $choice =~ m/(?i)q/ ) {
            $Is_Running = 0;
        }
    }

    sub game_enter {
        $Player_HP = 10;
        $Troll_HP  = 10;
    }

    sub game_render {
        say qq{"attack" the troll or "flee"};
    }

    sub game_update {
        my $choice = readline;
        if ( $choice =~ m/(?i)f/ ) {
            say qq{You find yourself back at the main menu...};
            pop @Game_State;
        } elsif ( $choice =~ m/(?i)a/ ) {
            my $roll = int rand(5);
            say "You hit the troll for $roll generic damage units!";
            $Troll_HP -= $roll;
            $roll = int rand(7);
            say "The troll hits you for $roll!";
            $Player_HP -= $roll;
            if ( $Player_HP <= 0 ) {
                say qq{You die...};
                say qq{... and so did the troll, congrats?} if $Troll_HP <= 0;
                state_pop();
                return;
            }
            if ( $Troll_HP <= 0 ) {
                say qq{You win! (this time...)};
                # this of course could instead go to a victory state or back
                # to the menu or whatever
                exit(0);
            }
        }
    }

state.pl