Sponsored By

StreamSDK Powered SNES w/Online Multiplayer Tutorial

How to convert a 30 year old SNES local multiplayer game into a modern cross-platform multiplayer game in under an hour with StreamSDK.

Jeremy Alessi, Blogger

April 10, 2020

8 Min Read
Game Developer logo in a gray background | Game Developer

Introduction

Marketing a new product can be difficult. It’s doubly difficult when it’s in a new space, like real-time streaming … if that’s even the right way to classify StreamSDK. Pursuant to this challenge, one way to reach people is to present novel new demos that showcase the power of the platform and play off of something previously known to people.

Cue the StreamSDK SNES Teenage Mutant Ninja w/ Online Multiplayer Demo. 

With this little piece, a nearly 30 year old game that was made for a console with no online capabilities can now be run on any platform and its two-player mode can be enjoyed between any two platforms from around the world.

In a nutshell, it’s a game people know, it’s a novel and powerful demonstration, and it highlights StreamSDK’s ability to empower things that just weren’t possible before.

Approach

Last weekend, the idea of converting older games to online multiplayer popped into mind. To be certain, this wasn’t the first time StreamSDK was used to convert a local multiplayer game into online multiplayer. During the first month of StreamSDK 3.0.0’s release, the Tanks demo from the Unity Asset Store as well as Swap Fire (Nintendo Wii U) both received the same treatment.

Of course, the Unity Tanks demo and Swap Fire are both pretty obscure. In short, they don’t fit the criteria of being previously known to people. Furthermore, it would be unsurprising for either of those Unity native games to re-emerge with online multiplayer capabilities.

So, the idea of instantly converting entire libraries of games, that were previously unplayable with online multiplayer, into online multiplayer was very intriguing. It also seemed like an impossibility at first. A short search quickly brought hope though. Sure enough, there was a MAME emulator written in C#. Wow! Maybe that could work with Unity?

No alt text provided for this image

More searching resulted in better prospects. It really seemed like there was a solution out there. Then, finally … a Game Boy emulator written directly in Unity appeared. The result was that an idea conceived at noon was actually functional by midnight (and there was a load of family time in between since it was a Sunday ;)

Acceleration

The StreamSDK Game Boy was neat, but to really achieve the original idea, the Super Nintendo would be golden. The idea of using the code structure from the Game Boy emulator to create a virtual SNES was an option but thankfully, more searching turned up Libretro and several Libretro powered Unity projects. Most didn’t work… and they were usually VR demos. Then finally, a bare bones example emerged called RetroUnity. It didn’t work out of the box, but by mixing and matching some elements from the other projects, it became functional. Making it work with StreamSDK after that was cake.

Technical (updated to represent StreamSDK3.1.0)

Now for the cream filling; just how does StreamSDK interface with a 30 year old game and make it online multiplayer?

  • Download RetroUnity project from GitHub

  • Import StreamSDK assets per docs at www.StreamSDK.com

  • Project View, Create Folder “StreamSDK/Experimental”

  • Project View, Drag “Plugins/RetroUnity into StreamSDK/Experimental”

  • Hierarchy, Create Empty

  • Rename, “RetroUnityContent”

  • Hierarchy, Drag all other game objects onto “RetroUnityContent”

  • Hierarchy, Make Prefab, Drag “RetroUnityContent” to Project View “StreamSDK/Experimental/RetroUnity”

  • Project View, Open “StreamSDK/Demo/Car3D/CarCloudcast.unity”

  • Hierarchy, Expand “Content”

  • Hierarchy, Delete children of “Content” (DO NOT DELETE StreamDisplayCanvas)

  • Project View, Drag “StreamSDK/Experimental/RetroUnity/RetroUnityContent.prefab” onto existing “Content” game object in the Hierarchy

  • Right Click, Unpack Prefab

  • Inspector, Set StreamSDK

  • Inspector, Set StreamSDK

  • Axes Size = 10

  • Element 0 = A

  • Element 1 = B

  • Element 2 = X

  • Element 3 = Y

  • Element 4 = DpadX

  • Element 5 = DpadY

  • Element 6 = START

  • Element 7 = SELECT

  • Element 8 = L

  • Element 9 = R

  • Stream Camera = (Scene) “Content/RetroUnityContent/Main Camera”

  • Hierarchy, Select StreamSDKTransporter

  • Inspector, Set StreamSDKTransporter

  • Room Name = SNES

  • Send Rate = 60

  • Project View, Open “StreamSDK/Experimental/RetroUnity/Scripts/LibreroWrapper.cs”

  • On ~line 30 Add

  • public bool streaming;

  • On ~line 43 Add

  • public static LibretroWrapper instance;

  • On ~line 65 Add

  • instance = this;

  • On ~line 362, Replace Method public static short RetroInputState with:

    public static short RetroInputState(uint port, uint device, uint index, uint id) {

if( instance == null || StreamSDK.instance == null )

return 0;

if( instance.streaming ) {

int i = (int)port;

switch (id) {

case 0:

return StreamSDK.GetAxis("B", i ) != 0 ? (short) 1 : (short)0;

case 1:

return StreamSDK.GetAxis("Y", i ) != 0 ? (short) 1 : (short)0;

case 2:

return StreamSDK.GetAxis("SELECT", i ) != 0 ? (short) 1 : (short)0;

case 3:

return StreamSDK.GetAxis("START", i ) != 0 ? (short) 1 : (short)0;

case 4:

return StreamSDK.GetAxis("DpadX", i ) >= 1.0f ? (short) 1 : (short)0;

case 5:

return StreamSDK.GetAxis("DpadX", i ) <= -1.0f ? (short) 1 : (short)0;

case 6:

return StreamSDK.GetAxis("DpadY", i ) <= -1.0f ? (short) 1 : (short)0;

case 7:

return StreamSDK.GetAxis("DpadY", i ) >= 1.0f ? (short) 1 : (short)0;

case 8:

return StreamSDK.GetAxis("A", i ) != 0 ? (short) 1 : (short)0;

case 9:

return StreamSDK.GetAxis("X", i ) != 0 ? (short) 1 : (short)0;

case 10:

return StreamSDK.GetAxis("L", i ) != 0 ? (short) 1 : (short)0;

case 11:

return StreamSDK.GetAxis("R", i ) != 0 ? (short) 1 : (short)0;

case 12:

return Input.GetKey(KeyCode.E) ? (short) 1 : (short) 0;

case 13:

return Input.GetKey(KeyCode.R) ? (short) 1 : (short) 0;

case 14:

return Input.GetKey(KeyCode.T) ? (short) 1 : (short) 0;

case 15:

return Input.GetKey(KeyCode.Y) ? (short) 1 : (short) 0;

default:

return 0;

}

return 0;

} else {

switch (id) {

case 0:

return Input.GetKey(KeyCode.Z) || Input.GetButton("B") ? (short) 1 : (short) 0; // B

case 1:

return Input.GetKey(KeyCode.A) || Input.GetButton("Y") ? (short) 1 : (short) 0; // Y

case 2:

return Input.GetKey(KeyCode.Space) || Input.GetButton("SELECT") ? (short) 1 : (short) 0; // SELECT

case 3:

return Input.GetKey(KeyCode.Return) || Input.GetButton("START") ? (short) 1 : (short) 0; // START

case 4:

return Input.GetKey(KeyCode.UpArrow) || Input.GetAxisRaw("DpadX") >= 1.0f ? (short) 1 : (short) 0; // UP

case 5:

return Input.GetKey(KeyCode.DownArrow) || Input.GetAxisRaw("DpadX") <= -1.0f ? (short) 1 : (short) 0; // DOWN

case 6:

return Input.GetKey(KeyCode.LeftArrow) || Input.GetAxisRaw("DpadY") <= -1.0f ? (short) 1 : (short) 0; // LEFT

case 7:

return Input.GetKey(KeyCode.RightArrow) || Input.GetAxisRaw("DpadY") >= 1.0f ? (short) 1 : (short) 0; // RIGHT

case 8:

return Input.GetKey(KeyCode.X) || Input.GetButton("A") ? (short) 1 : (short) 0; // A

case 9:

return Input.GetKey(KeyCode.S) || Input.GetButton("X") ? (short) 1 : (short) 0; // X

case 10:

return Input.GetKey(KeyCode.Q) || Input.GetButton("L") ? (short) 1 : (short) 0; // L

case 11:

return Input.GetKey(KeyCode.W) || Input.GetButton("R") ? (short) 1 : (short) 0; // R

case 12:

return Input.GetKey(KeyCode.E) ? (short) 1 : (short) 0;

case 13:

return Input.GetKey(KeyCode.R) ? (short) 1 : (short) 0;

case 14:

return Input.GetKey(KeyCode.T) ? (short) 1 : (short) 0;

case 15:

return Input.GetKey(KeyCode.Y) ? (short) 1 : (short) 0;

default:

return 0;

}

}

}

  • Save LibretroWrapper.cs

  • Open Unity > Project Settings > Input, Setup Axes:

  • A

  • B

  • X

  • Y

  • DpadX

  • DpadY

  • START

  • SELECT

  • L

  • R

  • Save Scene as “StreamSDK/Experimental/RetroUnity/RetroUnityStream.unity”

  • Hierarchy, Select StreamSDKTransporter

  • Inspector, Set StreamSDKTransporter

  • Room Name = SNES

  • Send Rate = 60

  • Save Scene as “StreamSDK/Experimental/RetroUnity/RetroUnityControl.unity”

 

Playtime!

No alt text provided for this image

With a few downloads, a few drag ’n drops, and some very minor code editing; 1992’s Teenage Mutant Ninja Turtles: Turtles in Time is now a cross-platform, online multiplayer title.

The “RetroUnityStream.unity” scene is a “caster” and the “RetroUnityControl.unity” scene is a “controller”.

Build and run the “caster” on one machine and then build and run the “controller” on other machines (anywhere in the world) to connect to the “caster” and play the game.

Poof! It’s like magic!

Conclusion

No alt text provided for this image

As the Genie from Aladdin would say, "Uh , ah, almost. There are a few, uh, provisos. Ah, a couple of quid pro quo".

StreamSDK’s mission is Any-to-Any™, the SNES demos are somewhat limited because Libretro is not Unity native. However, it does have wrappers for almost every platform in existence, so it’s not far off. At this current time the “caster” has been run from a Mac and a PC, but it seems to run considerably better from the PC; indicating that the PC wrapper for the SNES core library (a sub-component used by Libretro) is superior to its Mac counterpart. Other platform core dll’s have not been checked yet.

The “controller” on the other hand is completely cross-platform and can be run from any device. This tightly fits StreamSDK’s Any-to-Any™ mission. And, although the “caster” might be limited, it does not require a big infrastructure to be useful. It is still decentralized and able to be leveraged by anyone with a computer and an Internet connection.

This was a fun little experiment with a classic game and a testament to the ease of use as well as the power of StreamSDK. Thanks for reading, and as always send any questions, concerns, or thoughts to [email protected] 

Read more about:

Featured Blogs

About the Author

Jeremy Alessi

Blogger

Jeremy Alessi has over 15 years of experience developing video games. He began his career as an indie developing several titles including Aerial Antics, which was published by Garage Games, Scholastic, and Reflexive Entertainment. Aerial Antics was listed as a top 5 physics download in Computer Gaming World, nominated for Sim Game of the Year by Game Tunnel, and featured on the G4 series Cinematech. After developing PC and Mac based indie games Jeremy moved into the mobile space and created several hit titles for the iPhone including Crash for Cash and Skyline Blade, which have been played by millions. This experience was passed on in the book iPhone 3D Game Programming All in One in which Jeremy walks new developers through the entire process of developing an iPhone game from conception to completion. Next, Jeremy entered the world of serious games and delivered complete training projects to both the Marine Corps and the Department of Transportation. Jeremy is particularly proud of Virtual Bridge Inspection, which is valuable tool in infrastructure maintenance. The tool trains bridge inspectors how to identify and quantify defects as small as 6 hundredths of an inch on a span of nearly a 1/4 mile. Jeremy presented the VBI project at Unite 2011. In addition Jeremy is a regular freelance contributor for Gamasutra having created the Games Demystified series of articles amongst other things. Currently, Jeremy is running Friendly Dots, a mobile studio dedicated to making fun games for busy buddies using the latest asynchronous technologies. The studio's flagship title, friendly.fire, allows players to build, share, and destroy physics enabled fortresses housing the friendly dots characters. You can follow him on Twitter @jeremyalessi.

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like