Is this a safe way of using HTTP query parameters to build a SQL query?
It is safe. You have a white list of allowed values and ensure that the user provided input is on the list.

But I'd still do the white list checking as the very first thing, and then do the other processing later, so it would be:

$field = 'name';
$direction = 'asc';
if(preg_match('^([a-z]+)_(asc|desc)$', $_GET['order'], $matches)) {
    if(in_array($matches[0], array("name", "level", "vocation"))) {
        $field = $matches[0];
        $direction = $matches[1];
    }
}


What happens here is that the block splits up and validates the input. You can be sure that once you're past this block, $field and $direction can be trusted. An important point is to use $_GET['order'] exactly once, since accessing it multiple times may lead to errors that will leak non-validated data.