(2013-10-17) Bash Game Programming

*I meant to publish this a few weeks ago, but I was unfortunately busy.*

I decided that I would try and build a very simple game engine in Bash. The premise was a bit ridiculous, but Bash is more flexible than people often give it credit for.

Sure, it's not efficent; it's not pretty. But it does get the job done... More or less.

I'm a long way off completion yet, but I already have some of the fundamentals sorted.

The main ones being map generation and player movement.

For now it does not generate random levels (which would be far more rouge like); however, it is completely capable of interpreting what each of the (limited) characters in the board file do.

The main generation loop looks like this:

~~~~
function board_draw () {
        declare -i i j
        i=0
        j=0
        while read -n1 c; do
                [[ ${i} -eq ${width} ]] && i=0 && j=$[${j}+1] && printf "\n"
                printf "%c" "${c}"
                [[ "${c}" == "${nextdoor}" ]] && py[${map}]=${j} && px[${map}]=${i}
                [[ "${c}" == "${prevdoor}" ]] && cy[${map}]=${j} && cx[${map}]=${i}
                m[${j}]="${m[${j}]}${c}"
                (( i++ ))
        done < "${base}/maps/${map}.map"
        tput cup $(( ${height} + 5 )) 0
        printf "Use IJKL to move. Use 'q' to quit."
        return 0
}
~~~~

In actual fact, the map drawing doesn't do collision detection, but instead inserts each line into an array (m[]). You can then check m[LINE] for character X to determine the positions of things.

It also registers it's next and previous door locations into py[], px[] and cy[], cx[]. This allows a smoother transition between levels without requiring position 1:1 to always be used.

The actual size of the map is defined in the main loop -- though this may be changed at a later date to allow for different dimensions of levels.

The player movement can then be calculated based on the previously mentioned positional array.

I currently use 4 different functions to handle movement, though this could actually be improved significantly. The current code looks like this:

~~~~
## Remove the previous player position
function player_clear () {
        cell="${m[${y}]:${x}:1}"
        tput cup ${y} ${x}
        [[ ${cell} == "${empty}" ]] && printf "${empty}"
        [[ ${cell} == "${prevdoor}" ]] && printf "${prevdoor}"
        [[ ${cell} == "${nextdoor}" ]] && printf "${nextdoor}"
        return 0
}

## Draw the current player position
function player_draw () {
        tput cup ${y} ${x}
        printf "${player}\b"
        tput cup $(( ${height} + 6 )) 0
        return 0
}

## Up Movement Checks
function player_movement_up () {
        declare direction
        direction="${m[$(( ${y} - 1 ))]:${x}:1}"
        key="Up"
        [[ ${direction} == "${wall}" ]] && return 1
        [[ ${direction} == "${nextdoor}" ]] && map_change_next
        [[ ${direction} == "${prevdoor}" ]] && map_change_prev
        player_clear
        tput cup $(( ${height} + 6 )) 0
        (( y-- ))
        player_position
        player_draw
        tput cup $(( ${height} + 6 )) 0
        return 0
}

## Down Movement Checks
function player_movement_down () {
        declare direction
        direction="${m[$(( ${y} + 1 ))]:${x}:1}"
        key="Down"
        [[ ${direction} == "${wall}" ]] && return 1
        [[ ${direction} == "${nextdoor}" ]] && map_change_next
        [[ ${direction} == "${prevdoor}" ]] && map_change_prev
        player_clear
        tput cup $(( ${height} + 6 )) 0
        (( y++ ))
        player_position
        player_draw
        tput cup $(( ${height} + 6 )) 0
        return 0
}

## Right Movement Checks
function player_movement_right () {
        declare direction
        direction="${m[${y}]:$(( ${x} + 1 )):1}"
        key="Right"
        [[ ${direction} == "${wall}" ]] && return 1
        [[ ${direction} == "${nextdoor}" ]] && map_change_next
        [[ ${direction} == "${prevdoor}" ]] && map_change_prev
        player_clear
        tput cup $(( ${height} + 6 )) 0
        (( x++ ))
        player_position
        player_draw
        tput cup $(( ${height} + 6 )) 0
        return 0
}

## Left Movement Checks
function player_movement_left () {
        declare direction
        direction="${m[${y}]:$(( ${x} - 1 )):1}"
        key="Left"
        [[ ${direction} == "${wall}" ]] && return 1
        [[ ${direction} == "${nextdoor}" ]] && map_change_next
        [[ ${direction} == "${prevdoor}" ]] && map_change_prev
        player_clear
        tput cup $(( ${height} + 6 )) 0
        (( x-- ))
        player_position
        player_draw
        tput cup $(( ${height} + 6 )) 0
        return 0
}
~~~~

All movements register the global x and y variables (either +1 or -1, depending on the direction) and then redraws the player character. Before it does this tohugh, it uses the backspace character and replaces it with the map arrays character to clear the palyers previous position.

The last thing would be level transition, which I initially wasn't sure how to do. However, I now have a fairly decent system that, based on the previous door (-) and next door (+) coords, can put the player in a more seemless transition position.

Each map transition does require a full board redraw though.

~~~~
## Change Map to the Next Level
function map_change_next () {
        [[ ! -f "${base}/maps/$(( ${map} + 1 )).map" ]] && title_draw 2 && main
        (( map++ ))
        unset m
        clear
        board_draw
        x=${cx[${map}]}
        y=${cy[${map}]}
        player_position
        player_draw
        player_controls
        return 0
}

## Change Map to the Previous Level
function map_change_prev () {
        [[ ! -f "${base}/maps/$(( ${map} - 1 )).map" ]] && return 0
        (( map-- ))
        unset m
        clear
        board_draw
        x=${px[${map}]}
        y=${py[${map}]}
        player_position
        player_draw
        player_controls
        return 0
}
~~~~

These are both similar again, but the initial check is different. If, for some reason, map 1 has a previous door on it, the code wont do anything. It will act like the start doors in door. The next door, though, will display the "Game Over" screen if you try and go to map 4.

All in all, the actual functionality of the game is limited, but I'm pretty please with how the overall engine has come out (considering there aren't libaries for this sort of thing).

I could use something like the ncurses library to do better drawing, but I feel that would detract to much from what I want to achieve.

Once I've built it to a point where the language is actually a hinderance, I'll likely translate it over to C and continue development from there.

You can check out my progress on GitHub @@ https://github.com/edgleyUK/brouge/; brouge; here @@