www.idziorek.net | blog | contact

Monospaced Font and Pixel Display for Space Engineers

September 23, 2016

Closeup of standard Space Engineers LCD Panel, showing pink letters. Single Pixels can be determined.
Standard LCD Panel displaying monospaced text.

Pixel Precise Font

For everyone suffering from the lack of monospaced fonts and pixel-based displays in Space Engineers here I share my simple, yet effective solution: A simple helper class in C#, which I wrote yesterday night to solve this particular problem. As a bonus this also enabled using the LCD Screens for arbitrary pixel-precise output, even with animations, as you will see later.

Have fun and feel free to improve it!

The class can be easily embedded in your programmable blocks and allows simulating pixel-precise displays on the LCD and Text Panels. The class also ships with a beautiful set of retro-style compile time mono-spaced fonts.

First admire a few screenshot to get a first impression:

A Space Engineers Console showcasing our Font. It displays some ASCII styled bars.
Status bars showing normal and inverted font.
Two adjacent Space Engineers Consoles. One shows a sinus wave, demonstrating pixel precise output.
The Display to the right demonstrates animated pixel precise output.

The class exposes the following public methods:

Pixel Display supports Animations

Complete setup with 3 Panels a timer block and a programmable block
A complete Setup featuring 3 LCD Panels and supporting blocks.

With a timer block this approach can be used for beautiful animations too!

The solution is based on emulating pixels using very small fonts ~0.2 on regular LCD Panels. I use ‘[‘ and ‘.’ here, since they both have the same widths and work quite well to emulate on/off pixels.

Usage of the class is easy and straigth forward from your Main function:

// construct a new Screen class instace providing 
// the label of your panel and dimensions.
Screen s = new Screen("Your_LCD_1",215,59,this);   
 
// put text on specified pixel coordinates
s.put_text(10,10,"   Welcome to the Machine  ");
 
// refresh the LCD Panel
s.update();

C Sharp Class for the Space Engineers Font

And this is the C# class behind it. You will have to copy it into your programmable blocks. (Let me know if there is a more elegant way?)

Sidenote: If you wonder how I generated this numbers inside the letters array, you can read it in this seperate post about bitmap fonts generation.

class Screen
{
 
   public int width;
   public int height; 
    
   IMyTextPanel panel;
 
   char mark_pixel='[';
   char mark_empty='.';
 
   static MyGridProgram programm; 
   string output;
 
   public Screen(string panel_name, int _width, int _height,  MyGridProgram programRef)
   {
        width=_width;
        height=_height;
        programm=programRef;
        panel=programm.GridTerminalSystem.GetBlockWithName(panel_name) as IMyTextPanel;  
 
        output=new string(' ',0);
        for(int y=0;y<height;y++)
        {
             output+=new string(mark_empty,width);
             output+='\n';
        }
   }
 
    public void pixelOn(int x,int y)
    {
         StringBuilder sb = new StringBuilder(output); 
         sb[y*(width+1)+x]=mark_pixel;
         output = sb.ToString();
    }
 
    public void draw_rect(int x, int y, int width, int height)
    {
            for(int i=x;i<x+width;i++)
            {
                pixelOn(i,y);
                pixelOn(i,y+height-1);
            }
 
            for(int i=y;i<y+height;i++) 
            { 
                pixelOn(x,i); 
                pixelOn(width+x-1,i); 
            } 
 
 
    }
 
    public void put_text(int x, int y, string str)
    {
       int linefeed=0;
       for(int i=0;i<str.Length;i++) 
       {
         if(str[i]=='\n')
         {
               y+=7;
               linefeed=0;
               continue;
         }
 
         linefeed+=7;    
         put_letter(x+linefeed,y,str[i]);     
       } 
    } 
 
    public void put_text_inv(int x, int y, string str) 
    { 
       int linefeed=0; 
       for(int i=0;i<str.Length;i++)  
       { 
         if(str[i]=='\n') 
         { 
               y+=7; 
               linefeed=0; 
               continue; 
         } 
  
         linefeed+=7;     
         put_letter_inv(x+linefeed,y,str[i]);      
       } 
    }  
 
    public void put_letter(int x, int y, int ascii_code)
    {
           int letter=letters[ascii_code-0x20];
           
           for(int i=0;i<5;i++)
           for(int j=0;j<5;j++)
    
                         if(0<(letter & (int)Math.Pow(2,24-(i+j*5)) ))
                                pixelOn(x+i,y+j);
    }
 
    public void put_letter_inv(int x, int y, int ascii_code) 
    { 
           int letter=letters[ascii_code-0x20]; 
           draw_rect(x-1,y-1,7,7); // outline
           for(int i=0;i<5;i++) 
           for(int j=0;j<5;j++) if(0>=(letter & (int)Math.Pow(2,24-(i+j*5)) )) 
                                pixelOn(x+i,y+j); 
    } 
 
    public void update() 
    { 
         panel.WritePublicText(output, false); 
    } 
 
    //  monospace fonts starting from 0x20 (space)
    static readonly int[] letters = 
    {
        0,4329476,10813440,11512810,16398526,17895697,6632015,4325376,2232450,8523912,
        22483413,4357252,68,31744,4,1118480,15390382,4608142,15239320,31504446,
        1841462,33060926,33062463,32540808,33095231,33094719,131200,131208,2236546,
        1016800,8521864,32051204,15392270,33095217,32045630,33047071,32032318,
        33061407,33062416,33050175,18415153,14815374,14748236,20673235,17318431,18732593,18667121,
        15255086,32045584,15259213,32045779,33299071,32641156,18400814,18393412,18405233,18157905,18157700,
        32575775,14950670,17043521,14747726,4539392,31,6389760,33095217,32045630,
        33047071,32032318,33061407,33062416,33050175,18415153,14815374,14748236,
        20673235,17318431,18732593,18667121,15255086,32045584,15259213,32045779,33299071,
        32641156,18400814,18393412,18405233,18157905,18157700,32575775,2240642,4329604
        8525960,14016,
    };
     
};

Source Code used for Example Screenshots above

static int cnt=0; // programm counter
  
public void Main(string argument)
{ 
    // increase programm counter
    cnt+=1;
     if (cnt>100) cnt= 0;
 
    // init our screen class:  new 438x59 screen
    Screen s = new Screen("Miguel_LCD_1",(int)(438*1.5),(int)(59*1.5),this); 
 
    // draw some rectangles
    s.draw_rect(0,0,s.width,s.height);
    s.draw_rect(2,2,s.width-4,s.height-4);
 
     //draw more rectangles (stress testing only)
     //   for(int i=4; i<35; i+=2)
      //  {
      //        s.draw_rect(i,i,s.width-2*i,s.height-2*i);
       // }
 
    // write counter value
    s.put_text(8,5,"counter: "+cnt);
 
    // inverted text
    s.put_text_inv(8,12,"SOME INVERTED TEXT                       --- ");
    
     
    // paint a sinus wave
    for(int i=0;i<s.width;i++) { if(i>200)s.pixelOn(i,(int)(s.height/2+s.height/3*Math.Sin((i+cnt)/25.0)));
    }
 
    // paint individual letters along a sinus
    string txt=new string(' ',0); 
    txt="! software fools rules the waves!";
     
    for(int i=0;i<txt.Length;i++) { s.put_letter(30+i*7,(int)(s.height/2+s.height/5*Math.Sin((30+i*6+cnt)/25.0)),txt[i]); } s.update(); // other screens Screen s2 = new Screen("Miguel_LCD_2",329,89,this); Screen s3 = new Screen("Miguel_LCD_3",215,59,this); s3.put_text_inv(10,10," Welcome to the Machine "); s3.put_text(10,17,"-= Miguel's 5x5 Fonts =-\n The quick brown fox \n jumps over the lazy dog\n0123456789><=;'~^\n"+
    " %#@!~?{}[]-+*&`.,:'");
     
    s3.draw_rect(0,0,s3.width,s3.height);  
    s3.draw_rect(2,2,s3.width-4,s3.height-4);  
 
    s2.put_text(10,1,
"Something......[||||||||||      ]  10%\n"+
"Foo............[|||             ]  33%\n"+
"Bar............[|||             ]  99%\n"+
"Something else.[||||||||||I     ]  05%\n"
);
 
s2.put_text_inv(10,35, 
"Something......[||||||||||      ]  10%\n"+ 
"Foo............[|||             ]  33%\n"+ 
"Bar............[|||             ]  99%\n"+ 
"Something else.[||||||||||I     ]  05%\n"
); 
 
s2.put_text(10,65, 
"copyright by softwarefools.com"
); 
 
    s2.update();
    s3.update();
}

Helpful Comment on improving Performance

JR Raynal
December 13, 2017 at 9:29 am

Hi, I was playing with your script, and found it super slow… which is odd given the performances of other scripts out there. Turns out by switching the output from a string to a StringBuilder, you don’t need to copy them around as much. I assume this is the fix because I experienced a serious speed increase with that!

Here is the rewritten snippet:


            StringBuilder output;

            public Screen(string panel_name, int _width, int _height, MyGridProgram programRef)
            {
                width = _width;
                height = _height;
                programm = programRef;
                panel = programm.GridTerminalSystem.GetBlockWithName(panel_name) as IMyTextPanel;

                output = new StringBuilder();
                for (int y = 0; y < height; y++)
                {
                    output.Append(mark_empty, width);
                    output.Append('\n');
                }
            }

            public void pixelOn(int x, int y)
            {
                output[y * (width + 1) + x] = mark_pixel;
            }

TODOS

cleanup and optimize code!!, move source code to git repo, use some kind of syntax highlihter on this post. allow other font-sets with other sizes and let the user adjust spacing, publish python font converter in another post.