Befunge, 444 368 323 bytes
&1>\1-:v
0v^*2\<_$00p>
_>:10p\:20pv^_@#-*2g00:+1,+55$
^!-<v*2g000<>$#<0>>-\:v
g2*^>>10g20g+v \ ^*84g_$:88+g,89+g,\1+:00
v#*!-1g02!g01_4^2_
>::00g2*-!\1-:10g-\20g-++>v
87+#^\#p01#<<v!`g01/2\+76:_
vv1-^#1-g01:\_$:2/20g`!
_ 2/^>:10g#vv#`g02/4*3:\+77
v>0p^^/2:/2_
<^2-1-g02</2`#*3:
0g+10p2*:^*3_1
! "#%$
%$"#!
!!##%
|||_
_ __
Experimente online!
A abordagem típica para desenhar a curva de Hilbert é seguir o caminho como uma série de traços e curvas, renderizando o resultado em um bitmap ou em alguma área da memória e, em seguida, gravando essa renderização quando o caminho estiver completo. Isso não é viável no Befunge quando temos apenas 2000 bytes de memória para trabalhar, e isso inclui a fonte do próprio programa.
Portanto, a abordagem que adotamos aqui é criar uma fórmula que nos diga exatamente qual caractere produzir para uma determinada coordenada x, y. Para entender como isso funciona, é mais fácil de ignorar a prestação ASCII para começar, e apenas pensar na curva como composta de caracteres de caixa: ┌, ┐, └, ┘, │, e ─.
Quando olhamos para a curva assim, podemos ver imediatamente que o lado direito é um espelho exato do lado esquerdo. Os caracteres à direita podem ser simplesmente determinados procurando o parceiro à esquerda e refletindo-o horizontalmente (ou seja, ocorrências de ┌e ┐são trocadas, como são └e ┘).

Então, olhando para o canto inferior esquerdo, novamente podemos ver que a metade inferior é um reflexo da metade superior. Assim, os caracteres na parte inferior são simplesmente determinados procurando o parceiro acima e refletindo-o verticalmente (ou seja, ocorrências de ┌e └são trocadas, como são ┐e ┘).

A metade restante deste canto é um pouco menos óbvia. O bloco do lado direito pode ser derivado de uma reflexão vertical do bloco na diagonal adjacente a ele.

E o bloco da mão esquerda pode ser derivado de uma reflexão vertical do bloco no canto superior esquerdo da curva completa.

Neste ponto, tudo o que resta é o canto superior esquerdo, que é apenas mais uma curva de Hilbert uma iteração mais baixa. Em teoria, agora precisamos repetir o processo novamente, mas há um problema - nesse nível, as metades esquerda e direita do bloco não são espelhos exatos uma da outra.
Portanto, em qualquer coisa que não seja o nível superior, os caracteres do canto inferior precisam ser tratados como um caso especial, onde o ┌caractere é refletido como ─e o │caractere é refletido como └.

Mas, fora isso, podemos realmente repetir esse processo recursivamente. No último nível, codificamos o caractere superior esquerdo como ┌e o caractere abaixo dele como │.

Agora que temos uma maneira de determinar a forma da curva em uma determinada coordenada x, y, como podemos traduzir isso na renderização ASCII? Na verdade, é apenas um mapeamento simples que traduz cada bloco possível em dois caracteres ASCII.
┌torna-se _(espaço mais sublinhado)
┐torna-se (dois espaços)
└torna-se |_(barra vertical mais sublinhado)
┘torna-se | (barra vertical mais espaço)
│torna-se | (novamente uma barra vertical mais espaço)
─torna-se __(dois sublinhados)
Esse mapeamento não é intuitivo a princípio, mas você pode ver como ele funciona quando olha duas renderizações correspondentes lado a lado.

E isso é basicamente tudo o que existe. Na verdade, implementar esse algoritmo no Befunge é outro problema, mas deixarei essa explicação para outro momento.