Viper Virtual Console official website
Since November 2023, the Viper console has been undergoing a complete rewrite using the Godot Engine. This manual is a work in progress and may not fully reflect the current features of the console. However, the cheatsheet section does reflect the features of the current development version.
If you’re consulting this from the viper console, an online version of this manual can be found at https://viperconsole.github.io/index_dev.html
This is the 3.0.2a development version manual. You can browse the stable version (the one published at https://jice-nospam.itch.io/viper-console) at https://viperconsole.github.io
Viper is a virtual console inspired by early 90s hardware such as NEC/PCEngine, Amiga 500, and Neo-Geo. While its primary focus is on 2D, pixel-art-based video games, it remains versatile enough to accommodate the development of 3D engines.
Although some 90s limitations have been lifted—like the use of 32-bit colors instead of indexed palettes—the project strives to maintain the nostalgic early 90s aesthetic. Notably, Viper avoids employing the alpha channel extensively, limiting its usage to layer compositing and parallax effects.
Graphics :
- 384x224 screen resolution
- 32 bits colors with alpha channel support
- Display based on a set of transparent layers
- Layers can be moved and resized
- Layer operations supported (mix, mul, add, sub, ...)
- Any number of offscreen spritesheets
- Row/colscroll parameters for smooth parallax effects
Sound :
- 6 channels 44100Hz (native) or 16000Hz (web)
- Subtractive synthetizer
- Saw, triangle, square and noise generators
- Samples support (.wav)
- Midi controller input support
Input :
- Support mouse, keyboard and controller
Resources :
- Can load images (.png) and samples (.wav)
- Supports both local filesystem and web connectivity through HTTPS.
Code :
- Uses GDScript
- Full support of Godot API including coroutines
From the console main menu, open the code editor and paste the code below :
var x = 10
func draw() :
V.gfx.clear()
V.gfx.print(V.gfx.FONT_8X8, "Hello world!", Vector2(x,10))
Then press the Start button from the main menu to run the code.
The draw()
function is invoked every frame, and it’s the designated place to call all rendering methods from V.gfx
. Keep in mind that the framerate is not guaranteed and may vary across different target systems.
The update() function is designed for updating time-dependent variables and is called at a fixed rate of 60 times per second. This consistent rate ensures uniform game speed across all systems. Additionally, it’s the ideal place to manage user input using the V.inp API.
func update() :
x = 10 + V.elapsed()*50 # move at 50 pixels / second
There’s a third function available for setting up things before your game starts :
func init() :
# do some initialization stuff
pass
This is the designated place for loading images and samples. It’s recommended to perform these loading operations here to avoid pauses, particularly when fetching resources from HTTPS. This function can also call rendering functions, as it is executed during the rendering phase of the first frame.
Open the store from the main menu by clicking on and enter ‘tuto’ in the filter field. Here, you will discover a variety of commented tutorials that showcase the different features of the console.
The console features its own internal file system, allowing you to copy files through drag-and-drop directly onto the console window. For instance, you can drag an ‘spritesheet.png’ image onto the console window and then access it in your code simply as ‘spritesheet.png.’
This functionality also facilitates the effortless replacement of resources, be it a sample or an image, while running any game. By dropping a replacement file with the same name onto the window, the console seamlessly utilizes this file instead of the original resource.
You can also access files from outside the console using “file://<path>” syntax (only on native version of the console) or “https://<url>” to access online resources.
You don’t have to edit the code from the console editor, you can use an external editor and simply put the address of the file in the internal code editor :
Type file://C:/mygame.gd
to load the code from the C:\mygame.gd
file.
Type https://mysite.com/mygame.gd
to load the code from the given URL using an HTTP request.
You can use those arguments to control the console’s behavior. For instance, to enable fullscreen at launch :
Use viper.exe --fullscreen
in native mode.
Use https://html.itch.zone/html/9217920/index.html/?fullscreen=1
in web mode
Parameter | Description |
---|---|
Bios | |
cart=<path> |
Start a cartridge at launch. path = the .gd script to run |
no_deepswitch |
Disable deepswitch menu (ctrl-shift-`) |
no_docker |
Disable docker menu |
Parameter | Description |
---|---|
Graphics | |
no_crt |
Disable all CRT effects |
no_crt_warp |
Disable screen warping effect |
no_crt_scan |
Disable scanline effect |
no_crt_mask |
Disable shadow mask effect |
fullscreen |
Start in fullscreen mode |
no_splash |
Disable splash screen (only on native mode) |
no_vsync |
Disable vertical sync (only on native mode) |
Parameter | Description |
---|---|
Audio | |
no_audio |
Start with sound disabled |
audio_rate=<rate> |
Change the audio sample rate (value in Hz) |
Parameter | Description |
---|---|
Input | |
neutral_zone=<value> |
Change the controllers default neutral zone (value between 0.0 and 1.0) |
Key | Command |
---|---|
Escape Joypad 6* |
Toggle docker menu |
F12 | Save a screenshot in %USERPROFILE%\Pictures\Viper |
Ctrl-Shift-` | Toggle deepswitch menu |
X LMB Joypad 0* |
Action1 |
C RMB Joypad 2* |
Action2 |
V Mouse thumb 1 Joypad 1* |
Action3 |
W Mouse thumb 2 Joypad 3* |
Action4 |
* : Joypad buttons correspondance
Button | Sony | XBox | Nintendo |
---|---|---|---|
0 | cross | A | B |
1 | circle | B | A |
2 | square | X | Y |
3 | triangle | Y | X |
6 | start | menu | + |
Viper uses GDScript as scripting language.
See https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/index.html for more information
The following functions are called by the console :
func init()
: called once during the cartridge boot.This is where external resources should be loaded with V.gfx.load_img
for images and V.snd.new_instrument
for sound samples.
func update()
: called 60 times per secondThis is the designated place to update anything time-dependent. You can’t call drawing methods within this function.
func draw()
: called every frameRender one game frame. Framerate might vary from one computer to another. Use V.gfx.clear()
to erase the previous frame.
All the Viper API can be found under the V singleton :
V.gfx
: the graphics APIV.snd
: the audio APIV.inp
: the V.input APIIt also contains a few utilities :
V.fps() -> float
V.elapsed() -> float
Those functions return the number of frames rendered during the last second and the time elapsed in seconds since the game started. Note that you can display an fps counter on screen using the deepswitch menu.
The Viper screen size is fixed at 384x224 pixels, and you can retrieve these values using V.gfx.SCREEN_WIDTH
and V.gfx.SCREEN_HEIGHT
. The graphics engine supports displaying any number of layers with alpha blending.
All colors are expressed with float components but you can use the Color8 constructor to create one from byte values. While coordinates are stored as floats, for pixel-perfect results, it’s recommended to truncate them to integers. However, smooth movement can still be achieved using float coordinates.
Each layer is adjustable; you can resize them with V.gfx.set_layer_size
and reposition them with V.gfx.set_layer_offset
. Layers can be hidden or shown with V.gfx.show_layer
and V.gfx.hide_layer
. By default, only the first layer (layer 1) is displayed.
The console renders layers in increasing ID order, starting with layer 1, followed by layer 2, and so on. You can load spritesheets offscreen with V.gfx.load_img
and use them as source for blitting operation with V.gfx.set_spritesheet
and V.gfx.set_spritesheet_layout
. You can blit sprites with V.gfx.blit_region
and V.gfx.blit_sprite
You can fill a layer with a color with V.gfx.clear
. The default fill color is transparent.
Each visible layer applies a color operation between its color and the underlying color :
V.gfx.LAYEROP_MIX
: new pixel color = layer pixel color blended with background pixel color using the alpha channel valueV.gfx.LAYEROP_ADD
: new pixel color = background pixel color + layer pixel colorV.gfx.LAYEROP_SUBTRACT
: new pixel color = background pixel color - layer pixel colorV.gfx.LAYEROP_MULTIPLY
: new pixel color = background pixel color * layer pixel colorYou define the target layer for all drawing functions with V.gfx.set_active_layer(id)
.
Layers also have advanced features :
You can set per-row and per-column offsets using V.gfx.set_rowscroll
and V.gfx.set_colscroll
. These offsets can be utilized to create a per-row/column parallax effect on background layers.
You can configure a layer to render every pixel in a specific color using V.gfx.set_layer_blit_col
. This feature is useful for drawing a sprite shadow by utilizing the original sprite or coloring the entire sprite in white or red during a hit event.
You can draw various shapes with V.gfx.line
, V.gfx.triangle
, V.gfx.rectangle
, V.gfx.circle
.
You can blit sprites and images with V.gfx.blit_region
and V.gfx.blit_sprite
.
You can use V.gfx.blit_pixels
to blit a sprite generated from your code. However, it’s important to note that this function is significantly slower compared to V.gfx.blit_region
and V.gfx.blit_sprite
, which fully leverages GPU processing.
You define a bitmap font with set_font
by defining a rectangular zone inside the current spritesheet, and the character size :
set_font(char_size: Vector2i, rect: Rect2i = Rect2i(0,0,0,0), charset: String="", spacing: Vector2i = Vector2i.ZERO, char_width: PackedByteArray = []) -> int
Use the spacing
parameter to add additional space between characters and the char_width
parameter for proportional fonts. If charset
is not defined, the full ASCII table from 0 to 255 is expected.
The function returns a number representing this font. You can use this number to print text with V.gfx.print
.
The console is preloaded with three fonts :
V.gfx.FONT_8X8
: the default mono font that contains the complete 128 ascii table characters.
V.gfx.FONT_5X7
: a smaller non-mono font that contains the 93 ascii characters from !
to ~
.
V.gfx.FONT_4X6
: a very small mono font that contains the 93 ascii characters from !
to ~
.
The viper has 6 channels, each capable of playing stereo sound at 44kHz (16kHz on web platform) with float32 samples.
You can completely disable all sounds with V.snd.disable
.
You can create any number of instruments to produce sound using V.snd.new_instrument
. Simply pass a Dictionary as a parameter with various keys to define the sound.
Instrument can either utilize additive synthesis (TYPE = "Oscillator"
) through various oscillators or read samples from .wav files (TYPE = "Sample"
).
Oscillator instrument keys:
parameter | wave |
---|---|
- OVERTONE : amount of overtone (TODO)- OVERTONE_RATIO : 0 = octave below, 1 = fifth above (TODO) |
|
- SAW : amount of sawtooth waveform (0.0-1.0)- ULTRASAW : amount of ultrasaw in the saw waveform (0.0-1.0) (TODO) |
![]() |
- SQUARE : amount of square waveform (0.0-1.0)- PULSE : width of the square pulse should be in ]0..1[ interval |
![]() |
- TRIANGLE : amount of triangle waveform (0.0-1.0)- METALIZER : amount of metalizer in the triangle waveform (0.0-1.0) (TODO) |
![]() |
- SIN : amount of sin waveform (0.0-1.0) (TODO) |
![]() |
- ABS_SIN : amount of absolute sin waveform (0.0-1.0) (TODO) |
![]() |
- HALF_SIN : amount of half-sin waveform (0.0-1.0) (TODO) |
![]() |
- PSEUDO_SAW : amount of pseudo-saw waveform (0.0-1.0) (TODO)` |
![]() |
- NOISE : amount of noise (0.0-1.0)- NOISE_COLOR : from white (0.0) to pink (1.0) noise (TODO) |
filter parameters (TODO)
FILTER_BAND
: type of filter (LOWPASS
, BANDPASS
, HIGHPASS
, NOTCH
)FILTER_DISTO
: amount of distortion (0-200)FILTER_GAIN
: lowshelf filter boost in Db (-40, 40)FILTER_CUTOFF
: upper limit of frequencies getting a boost (0-8000)
lfo parameters (TODO)
LFO_AMOUNT
: amplitude of the lfo waveform (0-1)LFO_RATE
: frequency of the lfo waveform (0-8000)LFO_SHAPE
: type of waveform (SAW
, SQUARE
, TRIANGLE
)LFO_PITCH
: whether the lfo affects the notes pitch (0 or 1)LFO_FILTER
: whether the lfo affects the filter cutoff (0 or 1)LFO_PULSE
: whether the lfo affects the pulse width (0 or 1)LFO_METALIZER
: whether the lfo affects the metalizer amount (0 or 1)LFO_OVERTONE
: whether the lfo affects the overtone amount (0 or 1)LFO_ULTRASAW
: whether the lfo affects the ultrasaw amount (0 or 1)
envelop parameters
ATTACK
: duration of attack phase in secondsDECAY
: duration of decay phase in secondsSUSTAIN
: level of sustain phase between 0.0 and 1.0RELEASE
: duration of release phase in secondsBy default, envelop is altering the note volume. But it can also be used to alter other parameters (TODO) :
ENV_AMOUNT
: scale the effect of the envelop on the parameterENV_PITCH
: amount of envelop altering the note pitchENV_FILTER
: amount of envelop altering the note pitchENV_METALIZER
: amount of envelop altering the metalizer parameter of the oscillatorENV_OVERTONE
: amount of envelop altering the overtone parameter of the oscillatorENV_PULSE
: amount of envelop altering the pulse width of the oscillatorENV_ULTRASAW
: amount of envelop altering the utrasaw parameter of the oscillatorSample instrument keys :
FILE
: path to the .wav fileFREQ
: base frequency of the sound in the fileLOOP_START
: sample index where to start the loopLOOP_END
: sample index where to end the loopATTACK
: duration of attack phaseDECAY
: duration of decay phaseSUSTAIN
: duration of sustain phaseRELEASE
: duration of release phaseExamples :
var triangle_instrument_id = V.snd.new_instrument({TYPE="Oscillator", OVERTONE=1.0, TRIANGLE=1.0, METALIZER=0.85})
var snare_instrument_id = V.snd.new_instrument({TYPE="Sample",FILE="musics/samples/snare.wav",FREQ=440})
Once you defined instruments, you can play sound with V.snd.play_sound
and V.snd.play_note
.
play_note
take a Note object as parameter. You can create such an object with V.snd.parse_note
. See the pattern chapter for note format.
A pattern is a list of notes that plays at a specific rate.
new_pattern(description)
: register a new pattern and returns its id (0, then incremented for each new pattern).The description starts with the notes duration (or speed at which the pattern plays the notes). The duration is divided by 120. For example :
Then it’s followed by a list of notes. Example :
V.snd.new_pattern("02 F.207. G.207. D.307. C.407.")
The note format is [note] [octave] [instrument_id] [volume] [fx] with :
volume : hexadecimal between 1 (lowest) and F (highest)
you can define the balance by using two volume values :
Note that for more control over fade-in/fade-out effects, you can use an envelope on the instrument instead of effects. However, note that the envelope will affect all notes played with the instrument.
You can add a silence by filling the note format with 6 dots : ......
Example :
new_pattern("16 C#3915 ...... ...... C#3915 D#3835")
Once a pattern is defined, you can play it with V.snd.play_pattern
.
A music is a list of sequences, each sequence being a list of patterns being played simultaneously on different channels.
You define a song using V.snd.new_music
, using a Dictionary as parameter with following keys :
SEQ
is an array of 6 characters string, each describing which pattern to play on specific channels. Use a dot for unused channels.Example :
V.snd.new_music({SEQ=["07....","08...."]})
This music is composed of two sequences. The first one plays pattern 0 on channel 0 and pattern 7 on channel 1. The second plays pattern 0 on channel 0 and pattern 8 on channel 1.
Each channel pattern is encoded as an hexadecimal number between 0 and F.
If the music requires more than 16 patterns, you extends this limit to 256 (from 00
to FF
) using a double digit format with 12 characters strings :
V.snd.new_music({SEQ=["0007........","0008........"]})
If the song requires less than 16 patterns but those pattern ids are bigger than 16, you can use a PATLIST
key to define the list of patterns used by the music :
V.snd.new_music({PATLIST=[20,21,22],SEQ=["01....","02...."]})
Sequence 2 : pattern 20 on channel 0 and pattern 22 on channel 1
You can play a music with V.snd.play_music
. The channel_mask defines the channels to be used to play the music patterns. There must be enough channel to play all the simultaneous patterns in every sequence of the music.
For example with the previous song, which requires 3 channels, you can use the binary mask 000011 = 3. This would result in the channels 0 and 1 being reserved for the song.
TODO
key(key)
: return true if key is pressed. key is a value from Godot’s Key enumkey_pressed(key)
: return true if key was pressed during last framekey_released(key)
: return true if key was released during last frameThe value for the key
scan codes are :
V.inp.KEY_0
..V.inp.KEY_9
: upper digitsV.inp.KEY_A
..V.inp.KEY_Z
: letters (this returns KEY_Q when you press A on an AZERTY keyboard)V.inp.KEY_F1
..V.inp.KEY_F10
: function keysArrow keys : (also triggered by numeric keypad if numlock is disabled)
V.inp.KEY_UP
V.inp.KEY_DOWN
V.inp.KEY_LEFT
V.inp.KEY_RIGHT
Special keys :
V.inp.KEY_TAB
V.inp.KEY_SPACE
V.inp.KEY_BACKSPACE
V.inp.KEY_ENTER
V.inp.KEY_ESCAPE
V.inp.KEY_PRINTSCREEN
V.inp.KEY_SCROLLLOCK
V.inp.KEY_PAUSE
V.inp.KEY_INSERT
V.inp.KEY_HOME
V.inp.KEY_DELETE
V.inp.KEY_END
V.inp.KEY_PAGEDOWN
V.inp.KEY_PAGEUP
V.inp.KEY_CTRL
V.inp.KEY_LSHIFT
V.inp.KEY_RALT
V.inp.KEY_RSHIFT
List of button values :
Functions :
mouse_button(num)
: return true if mouse button num is pressed.You can use any button number or the predefined constants :
V.inp.MOUSE_LEFT
V.inp.MOUSE_MIDDLE
V.inp.MOUSE_RIGHT
mouse_button(num)
: return true if mouse button num was pressed since the last game tickmouse_button_released(num)
: return true if mouse button num was released since the last game tickmouse_pos()
: return the mouse position in pixelsExample :
local mousex,mousey = V.inp.mouse_pos()
Note : you can show/hide the mouse cursor with the V.gfx API :
show_mouse_cursor(onoff)
Example :
V.gfx.show_mouse_cursor(false)
In the following functions, player_num
value is :
0
: the keyboard (arrows for directions and X,C,V,Z,Escape,Enter corresponding to A,X,B,Y,Select,Start on the controller)1
to 8
: up to 8 controllers supportThis makes it possible to use the same API whether the player is using the keyboard or the controller.
Functions :
pad_button(player_num, num)
: return true if button num for player player_num is pressedpad_button_pressed(player_num, num)
: return true if button num for player player_num was pressed since the last game tickYou can use these constants for the num value :
V.inp.XBOX360_A
V.inp.XBOX360_B
V.inp.XBOX360_X
V.inp.XBOX360_Y
V.inp.XBOX360_LB
V.inp.XBOX360_RB
V.inp.XBOX360_SELECT
V.inp.XBOX360_START
V.inp.XBOX360_LS
V.inp.XBOX360_RS
pad_ls(player_num)
: return the left analog stick axis values (between -1 and 1) for player player_numExample :
local x,y = V.inp.pad_ls(1)
set_ls_neutral_zone(value)
: sets the left analog stick neutral zone (between 0 and 1)pad_rs(player_num)
: return the right analog stick axis values (between -1 and 1) for player player_numset_rs_neutral_zone(value)
: sets the right analog stick neutral zone (between 0 and 1)pad_lt(player_num)
: return the left trigger value (between -1 and 1) for player player_numpad_rt(player_num)
: return the right trigger value (between -1 and 1) for player player_numIf you want to support both keyboard and controller at the same time in a single player game, you can use these generic functions instead :
V.inp.action1()
returns true if the controller A button or keyboard X key are pressedV.inp.action2()
returns true if the controller X button or keyboard C key are pressedV.inp.action3()
returns true if the controller B button or keyboard V key are pressedV.inp.action4()
returns true if the controller Y button or keyboard Z key are pressedV.inp.menu()
returns true if the controller menu button or keyboard ESC key are pressedThe same functions with the _pressed suffix exist to check if the button was pressed since the last game tick. They all return a boolean.
V.inp.action1_pressed()
V.inp.action2_pressed()
V.inp.action3_pressed()
V.inp.action4_pressed()
V.inp.menu_pressed()
Those last function will check controller #1 and keyboard if the player is not defined, else only the controller #player (0=keyboard, 1-8=controller)
V.inp.up([player])
value between 0 and 1V.inp.down([player])
value between 0 and 1V.inp.left([player])
value between 0 and 1V.inp.right([player])
value between 0 and 1V.inp.up_pressed([player])
V.inp.down_pressed([player])
V.inp.left_pressed([player])
V.inp.right_pressed([player])
How can I generate 1D/2D/3D noise ?
See https://docs.godotengine.org/en/stable/classes/class_noise.html
Are there tweening utilities ?
You can use the Tween
class. Example :
var value = Tween.interpolate_value(start_value, delta_value, t, duration, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
See https://docs.godotengine.org/en/stable/classes/class_tween.html
How can I save data locally ?
See https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html