Making a game in Fortran.

A tale of partial OpenGL bindings in Fortran 2003

September 2024
>
Fortran has been around for ages. It began around late 1953 and early 1954 as a revolution in "automatic programming" in a day when code was written using mnemonic devices of raw assembly instructions [1]. To most it was believed that the inginuity of humanity was required to create efficient software. But after pulling all-nighters in the Langdon Hotel in spring 1956, John Backus and others managed to produce a working compiler in early 1957. About which John remarked
[...] we were often astonished at the surprising transformations in the indexing operations and in the arrangement of the computation which the compiler made, changes which we would not have thought to make as programmers ourselves [...]. Transfers of control appeared which corresponded to no source statement, expressions were radically arranged , and the same DO statement might produce no instructions in the object program in one context, and in another it would produce many instructions in different places in the program.
They had shown a compiler could produce better code than an expert human. Fortran in 1957 worked on punchcards, literally a piece of card with some holes in it to represent the source code data. Key to the language design was the use of an algebraic syntax, the clue is in the name FORmula TRANslation. But also the use of input and output statments, integers and floating point numbers, mathematical operators like \(+, -, =, >, <\), arrays a(i, j), subroutines and more. Since the 50's Fortran has gone through many revisions to add new features such as character data in the 77 standard, control flow structures and modules in the 90 standard, parallel computing built-ins in the 95 standard and even object-oriented programming in the 03 standard (there is even talk of C++ like templates in future standards [2-3]) with the 2023 standard as the most recent [4].

In spite of its continued development it has the reputation of a dated, obsolete language, used by perhaps only a handful of old developers. And yet Fortran is used extensively in many scientific contexts, in part due to inertia. If very efficient and complex code has solved a problem, why re-implement it again? One example domain is Quantum chemisty and solid state physics, where many CPU and GPU based software is still maintained in Fortran [5]. Another example are the excellent linear algebra routines and interfaces written in Fortran such as BLAS/LAPACK [6, 7] which have been the workhorse of many applications, including especially a lot of machine learning (although now of course new CPU/GPU libraries have been developed).

In short Fortran was built for numerical computing, and it has been very successful at it. So how do we get to a game in such a language? It turns out that Prof. Anthony Stone (Cambridge) and Aleksandar Donev maintained Fortran 2003 bindings for OpenGL [8] stemming from Fortran 90 standard bindings for OpenGL, from William F. Mitchell [9]. Amazingly if you download the source code, at least on Ubuntu 22.04, and run make you will be greated with a series of reasonably complex demos. If we can draw stuff, then we can make a game! Let's do something really simple, fortranBird - a Flappy Bird clone.
Demo of F03GL
Figure 1 - F03GL in action.
To get started, Fortran works like most other compiled languages. The entry point is typically written as

! Fortran is case in-sensistive, the capitalisation is a style choice.
Program main
! NB i is implicitly an Integer, see Implicit None
Do i = 1, 10
  Print *, "Hello, World!"
End Do

End Program
> gfortran t.F90 && ./a.out
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!
  Hello, World!

Which works as might be expected. The code may be organised into the entrypoint, a set of modules, functions, and subroutines. For the world geometry and the player we'll want to use some basic drawable unit with collision detection. Quads are a pretty versatile choice for this purpose so let's define a Quad type in the Quads module.

Module Quads
  ! A module for drawing untextured Quads
  Use opengl_gl
  Use opengl_glu
  Use opengl_glut

  ! So that i, j, k, n, ... are not Integer by default.
  Implicit None

  ! NB the module name and anything within it cannot have the same name.
  Type, Public :: Quad
      Real, Private                   :: cx, cy, w, h, theta, r, g, b, a
      Real, Private                   :: rot(1:2,1:2)
      Real, Private, Dimension(1:2,1) :: bl, tl, tr, br

      ! Subroutines and Functions of Quad.
      Contains
      Private
          ! &, is a line continuation character.
          ! By default gfortran has a max line length of 132.
          Procedure, Public :: draw, set_rotation,&
                                     set_position,&
          ! => makes init the external alias name for init_quad.
                                     init => init_quad
          Procedure, Public :: get_br,&
                               get_w,&
                               get_c,&
                               get_wh
  End Type Quad

  Contains
  ! Snipped member Subroutines and Function definitions... 

Again just like other languages we have a type with member data and member methods. The init Subroutine takes position, colour, data etc to setup the quad. The draw method is as follows

Subroutine draw(q)
  Class(Quad), Intent(InOut) :: q
  ! Old OpenGL speak to draw a quad.
  Call glBegin(GL_QUADS)
      ! % is the member access unary operator in Fortran.
      Call glColor4f(q%r,q%g,q%b,q%a)
      Call glVertex2f(q%bl(1,1), q%bl(2,1))
      Call glVertex2f(q%tl(1,1), q%tl(2,1))
      Call glVertex2f(q%tr(1,1), q%tr(2,1))
      Call glVertex2f(q%br(1,1), q%br(2,1))
  Call glEnd()
End Subroutine draw

It is a Subroutine, which means that it is a unit of code that takes inputs and does something to those inputs. There is no explicit return value. Instead we must give each argument an Intent which may be one or both of In and Out. Given a Quad initialised with some position and colour data q%draw() will display it on screen as expected. If the necessary GLUT incantations are used to setup the window.

Call glutInitWindowSize(wW,wH)
Call glutInit()

type = GLUT_RGB
type = ior(type,GLUT_SINGLE)

Call glutInitDisplayMode(type)
window = glutCreateWindow("Fortran Bird!")

Call glutDisplayFunc(display)
Call glutKeyboardFunc(keyDown)
Call glutKeyboardUpFunc(keyUp)

Call glutMainLoop()

Two random boxes in a window
Figure 2 - Some obstacles.
There are two distinct entity types in the game, obstacles and the bird. For the obstacles we want randomly sized quads at random positions that move to the left of the screen. With enough spacing for the player to slip through. These obstacles can be stored in an array, and progressively moved to the left as the game runs. Generation can be done in a Function (in this case as a member of a World type to store obstacles and update them).

Type(Quad) Function generate_obstacle(neighbour, space, height, &
  min_width, max_width, min_height, max_height)
  Type(Quad), Intent(In   ) :: neighbour
  Real,       Intent(In   ) :: space, height,&
                                min_width, max_width, min_height, max_height
  ! Data to be used in the Subroutine, any initialisation in these definitions
  ! makes the data static.
  Real, Dimension(1:2) :: c, wh

  c = neighbour%get_c()

  c(1) = c(1) + space + random()*space + neighbour%get_w()
  c(2) = random()*height

  wh(1) = random()*(max_width-min_width)+min_width
  wh(2) = random()*(max_height-min_height)+min_height

  Call generate_obstacle%init(&
    (/c(1), c(2)/),&
    (/wh(1), wh(2)/),&
    0.0,&
    colour)

End Function generate_obstacle

Note that the function's return value is its name generate_obstacle, which we call init on at the end. We could also use a plain Return statement as well. The next vital entity is the bird. We'll call it a Player and give it a body that is a Quad and two triangles, one for a beak and a wing.

Type, Public :: Player
  Real, Private                   :: cx, cy, w, h, theta
  Real, Private                   :: rot(1:2,1:2)
  Type(Quad), Private             :: body, bb
  Real, Private, Dimension(1:2,3) :: wing, beak
  Real, Private, Dimension(1:4)   :: wing_colour, body_colour, beak_colour

  Contains
  Private
      Procedure, Public :: draw, set_rotation, set_position, init => init_player
      Procedure, Public :: get_w, get_c, get_bb
End Type Player

Contains

  Subroutine draw(q)
    Class(Player), Intent(InOut) :: q
    Call q%body%draw()
    Call glBegin(GL_TRIANGLES)
      Call glColor4f(q%beak_colour(1), q%beak_colour(2), &
        q%beak_colour(3), q%beak_colour(4))
      Call glVertex2f(q%beak(1,1), q%beak(2,1))
      Call glVertex2f(q%beak(1,2), q%beak(2,2))
      Call glVertex2f(q%beak(1,3), q%beak(2,3))
    Call glEnd()
    Call glBegin(GL_TRIANGLES)
      Call glColor4f(q%wing_colour(1), q%wing_colour(2), &
        q%wing_colour(3), q%wing_colour(4))
      Call glVertex2f(q%wing(1,1), q%wing(2,1))
      Call glVertex2f(q%wing(1,2), q%wing(2,2))
      Call glVertex2f(q%wing(1,3), q%wing(2,3))
    Call glEnd()
  End Subroutine draw

The rest of the module being utilities for moving etc. To wrap this into a game we need a core gameplay loop. Here it is simple, each frame:
  • On pressing WASD the bird moves.
  • The obstacles move to the left at increasing speed.
  • The bird is pushed downwards.
  • If the bird collides with an obstacles that is game over.
Most of which is fairly simple to implement in any language using GLUTs update loop and I/O, and checking bounding box collisions. In the end, adding the play time as a score and text to show it on game over we have a game. Written in Fortran!
fortranBird
Figure 3 - FortranBird!
You can find the code and some pre-built binaries on Github https://github.com/JerboaBurrow/FortranBird which likely won't work on your linux or windows machine, but maybe you'll be lucky
[1] Backus, J., 1978. The history of Fortran I, II, and III. ACM Sigplan Notices, 13(8), pp.165-180. https://doi.org/10.1145/960118.808380
[2] j3-fortran (Github)
[3] The state of Fortran generics, Brad Richardson, July 2022, wordpress
[4] Fortran 2023, https://wg5-fortran.org/f2023.html
[5] https://en.wikipedia.org/wiki/List_of_quantum_chemistry_and_solid-state_physics_software
[6] BLAS https://github.com/OpenMathLib/OpenBLAS/tree/develop
[7] LAPACK https://github.com/Reference-LAPACK/lapack
[8] F03GL, Anthony Stone and Aleksandar Donev https://www-stone.ch.cam.ac.uk/pub/f03gl/index.xhtml
[9] f90gl, William F, Mitchell, NIST, https://math.nist.gov/f90gl/