Sliding Puzzle OBS Streamer Bot Thing
A way to allow you to turn any OBS source into a sliding puzzle controlled through Streamer Bot Sometimes a section of your stream interface is boring. Sometimes you need a way to raise your engagement. Sometimes you just allow the intrusive thoughts to take root and you want to make a bad idea barely work. In all of these scenarios, the correct response is to implement The Sliding Puzzle OBS Streamer Bot Thing. Give your viewers a minigame to play made of whatever you’re trying to show them. All of this was created 9 months or so before the blog post was written, so the details are a bit hazy. The plan is to combine Streamer.bot’s ability to run code on a trigger to send events to OBS through the built in websocket connections using their C# functionality. First, the Split function is run. This function finds the relevant source and grabs a bunch of metadata, like transforms and locations and offsets. After it gets this information, it clones the source 8 times and applies some math to each of those transforms to turn each of them into a chunk of 1/9 of the source. During that creation, it sets a bunch of variables to keep track of where the empty space is, and which scene item is in each spot on the grid. Finally, it hides the original source because you probably don’t want to actually mess with the original. The other part of the puzzle is the sliding actions. Each of these kind of do the same thing, but just manipulate the offsets in slightly different ways. First, check if the empty space is on an edge that would make the movement impossible (like sliding a piece up when the empty space is on the bottom row). Assuming the move is relevant, we fetch the scene item transforms for the piece that will be moving into the empty position, apply some math to the location, and push the new values to OBS then update the global variable state. The global state should stay up to date through all of the moves as long as you don’t run split again, so you can just hide the child scene items and unhide the parent to get the input back to normal, and vice-versa to re-enable. First, you need to figure out the sceneItemId for the thing that you want to turn into a sliding puzzle. The easy way to do this is to hook into OBS through the OBS Raw Streamer Bot page which takes in a scene name and returns all of the items. You can figure out the name of the relevant one by checking the sourceName values. Grab that and head over to the global variables section and add all of these settings. Create an action to do the Split with this code implemented as a sub-action. Finally, create actions for the slides with the following sub-actions I have never worked with OBS or Streamer.bot before and this seemed like a good crash course. Yeah, almost assuredly. If you want to do that then run split again and reset it. User persistent storage might work too? I am reasonably sure that it needs to be stateless so everything has to be written off somewhere. Maybe! You can probably just update the location in the movement direction in a loop or something. If I try that then I can make a follow up post. Or you can do it and let me know. If it can, let me know. The C# functionalty in Streamer.bot is pretty obtuse and I didn’t see a good way to make a reusable function to call from each slide method with parameters. I also did not look particularly hard.Huh?
The Solution
The Technical Bits
Global Variables
Global variables
slider_currentX = -1
slider_currentY = -1
slider_matrix00 = -1
slider_matrix01 = -1
slider_matrix02 = -1
slider_matrix10 = -1
slider_matrix11 = -1
slider_matrix12 = -1
slider_matrix20 = -1
slider_matrix21 = -1
slider_matrix22 = -1
slider_SceneName = [Your Scene Name]
slider_SourceSceneItemId = [the sceneItemId we got earlier]
slider_xCropDelta = -1
slider_yCropDelta = -1Splits
slider_SplitSubAction.cs
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
var sceneName = CPH.GetGlobalVar<string>("slider_SceneName", true);
var sceneItemId = CPH.GetGlobalVar<string>("slider_SourceSceneItemId", true);
//Duplicate, set global vars
for (int i = 0; i < 8; i++)
{
var responseStr = CPH.ObsSendRaw("DuplicateSceneItem", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId},\"destinationSceneName\":null}}", 0);
CPH.LogDebug(responseStr.ToString());
JObject resp = JObject.Parse(responseStr);
var itemId = (int)resp["sceneItemId"];
var matrixId = $"{(int)i / 3}{i % 3}";
CPH.LogDebug($"Item Id: {itemId}, Matrix Id: {matrixId}");
CPH.SetGlobalVar($"slider_matrix{matrixId}", itemId, true);
}
//Get some offsets
var itemTransformResp = CPH.ObsSendRaw("GetSceneItemTransform", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId}}}", 0);
JObject originalTransform = JObject.Parse(itemTransformResp);
var xOrigin = (int)originalTransform["sceneItemTransform"]["positionX"];
var yOrigin = (int)originalTransform["sceneItemTransform"]["positionY"];
var xCropDelta = (int)((int)(originalTransform["sceneItemTransform"]["sourceWidth"]) / 3);
var yCropDelta = (int)((int)(originalTransform["sceneItemTransform"]["sourceHeight"]) / 3);
CPH.LogDebug($"{xOrigin} - {yOrigin} - {xCropDelta} - {yCropDelta}");
//Clean up each one
for (int i = 0; i < 8; i++)
{
var matrixId = $"{(int)i / 3}{i % 3}";
var tmpSceneItemId = CPH.GetGlobalVar<int>($"slider_matrix{matrixId}", true);
//Let's get weird with JObject copying
var newFancyTransform = JObject.FromObject(originalTransform).DeepClone().ToObject<JObject>();
//Okay they send back 0 but require a minimum value of 1 even though you send back NO_BOUNDS ??? That took a hot minute to figure out
newFancyTransform["sceneItemTransform"]["boundsWidth"] = 1;
newFancyTransform["sceneItemTransform"]["boundsHeight"] = 1;
//
newFancyTransform.Add("sceneName", sceneName);
//
newFancyTransform["sceneItemId"] = tmpSceneItemId;
newFancyTransform["sceneItemTransform"]["cropLeft"] = (int)newFancyTransform["sceneItemTransform"]["cropLeft"] + (((int)i / 3) * xCropDelta);
newFancyTransform["sceneItemTransform"]["cropRight"] = (int)newFancyTransform["sceneItemTransform"]["cropRight"] + ((2 - ((int)i / 3)) * xCropDelta);
newFancyTransform["sceneItemTransform"]["cropTop"] = (int)newFancyTransform["sceneItemTransform"]["cropTop"] + ((i % 3) * yCropDelta);
newFancyTransform["sceneItemTransform"]["cropBottom"] = (int)newFancyTransform["sceneItemTransform"]["cropBottom"] + ((2 - (i % 3)) * yCropDelta);
newFancyTransform["sceneItemTransform"]["positionX"] = (int)((int)newFancyTransform["sceneItemTransform"]["positionX"] + (((int)i / 3) * xCropDelta) * (double)newFancyTransform["sceneItemTransform"]["scaleX"]);
newFancyTransform["sceneItemTransform"]["positionY"] = (int)((int)newFancyTransform["sceneItemTransform"]["positionY"] + ((i % 3) * yCropDelta) * (double)newFancyTransform["sceneItemTransform"]["scaleY"]);
string tmpJson = newFancyTransform.ToString();
CPH.ObsSendRaw("SetSceneItemTransform", tmpJson, 0);
}
//Make the original invisible
CPH.ObsSendRaw("SetSceneItemEnabled", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId},\"sceneItemEnabled\":false}}", 0);
CPH.SetGlobalVar($"slider_currentX", 2, true);
CPH.SetGlobalVar($"slider_currentY", 2, true);
CPH.SetGlobalVar($"slider_xCropDelta", xCropDelta, true);
CPH.SetGlobalVar($"slider_yCropDelta", yCropDelta, true);
return true;
}
}Slides
slider_DownSubAction.cs
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
var sceneName = CPH.GetGlobalVar<string>("slider_SceneName", true);
var currentX = CPH.GetGlobalVar<int>("slider_currentX", true);
var currentY = CPH.GetGlobalVar<int>("slider_currentY", true);
var xCropDelta = CPH.GetGlobalVar<int>("slider_xCropDelta", true);
var yCropDelta = CPH.GetGlobalVar<int>("slider_yCropDelta", true);
if (currentY == 0)
{
return true;
}
var sceneItemId = CPH.GetGlobalVar<int>($"slider_matrix{currentX}{currentY - 1}", true);
CPH.LogDebug($"{sceneName} - {currentX} - {currentY} - {xCropDelta} - {yCropDelta} - {sceneItemId}");
var itemTransformResp = CPH.ObsSendRaw("GetSceneItemTransform", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId}}}", 0);
JObject originalTransform = JObject.Parse(itemTransformResp);
originalTransform["sceneItemTransform"]["positionY"] = (int)originalTransform["sceneItemTransform"]["positionY"] + (yCropDelta * (double)originalTransform["sceneItemTransform"]["scaleY"]);
originalTransform["sceneItemTransform"]["boundsWidth"] = 1;
originalTransform["sceneItemTransform"]["boundsHeight"] = 1;
//
originalTransform.Add("sceneName", sceneName);
originalTransform.Add("sceneItemId", sceneItemId);
var tmpJson = originalTransform.ToString();
CPH.LogDebug(tmpJson);
CPH.ObsSendRaw("SetSceneItemTransform", tmpJson, 0);
//Write new state in global vars
CPH.SetGlobalVar("slider_currentY", currentY - 1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY - 1}", -1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY}", sceneItemId, true);
return true;
}
}slider_UpSubAction.cs
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
var sceneName = CPH.GetGlobalVar<string>("slider_SceneName", true);
var currentX = CPH.GetGlobalVar<int>("slider_currentX", true);
var currentY = CPH.GetGlobalVar<int>("slider_currentY", true);
var xCropDelta = CPH.GetGlobalVar<int>("slider_xCropDelta", true);
var yCropDelta = CPH.GetGlobalVar<int>("slider_yCropDelta", true);
if (currentY == 2)
{
return true;
}
var sceneItemId = CPH.GetGlobalVar<int>($"slider_matrix{currentX}{currentY + 1}", true);
CPH.LogDebug($"{sceneName} - {currentX} - {currentY} - {xCropDelta} - {yCropDelta} - {sceneItemId}");
var itemTransformResp = CPH.ObsSendRaw("GetSceneItemTransform", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId}}}", 0);
JObject originalTransform = JObject.Parse(itemTransformResp);
originalTransform["sceneItemTransform"]["positionY"] = (int)originalTransform["sceneItemTransform"]["positionY"] - (yCropDelta * (double)originalTransform["sceneItemTransform"]["scaleY"]);
originalTransform["sceneItemTransform"]["boundsWidth"] = 1;
originalTransform["sceneItemTransform"]["boundsHeight"] = 1;
//
originalTransform.Add("sceneName", sceneName);
originalTransform.Add("sceneItemId", sceneItemId);
var tmpJson = originalTransform.ToString();
CPH.LogDebug(tmpJson);
CPH.ObsSendRaw("SetSceneItemTransform", tmpJson, 0);
//Write new state in global vars
CPH.SetGlobalVar("slider_currentY", currentY + 1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY + 1}", -1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY}", sceneItemId, true);
return true;
}
}slider_LeftSubAction.cs
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
var sceneName = CPH.GetGlobalVar<string>("slider_SceneName", true);
var currentX = CPH.GetGlobalVar<int>("slider_currentX", true);
var currentY = CPH.GetGlobalVar<int>("slider_currentY", true);
var xCropDelta = CPH.GetGlobalVar<int>("slider_xCropDelta", true);
var yCropDelta = CPH.GetGlobalVar<int>("slider_yCropDelta", true);
if (currentX == 2)
{
return true;
}
var sceneItemId = CPH.GetGlobalVar<int>($"slider_matrix{currentX + 1}{currentY}", true);
CPH.LogDebug($"{sceneName} - {currentX} - {currentY} - {xCropDelta} - {yCropDelta} - {sceneItemId}");
var itemTransformResp = CPH.ObsSendRaw("GetSceneItemTransform", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId}}}", 0);
JObject originalTransform = JObject.Parse(itemTransformResp);
originalTransform["sceneItemTransform"]["positionX"] = (int)originalTransform["sceneItemTransform"]["positionX"] - (xCropDelta * (double)originalTransform["sceneItemTransform"]["scaleX"]);
originalTransform["sceneItemTransform"]["boundsWidth"] = 1;
originalTransform["sceneItemTransform"]["boundsHeight"] = 1;
//
originalTransform.Add("sceneName", sceneName);
originalTransform.Add("sceneItemId", sceneItemId);
var tmpJson = originalTransform.ToString();
CPH.LogDebug(tmpJson);
CPH.ObsSendRaw("SetSceneItemTransform", tmpJson, 0);
//Write new state in global vars
CPH.SetGlobalVar("slider_currentX", currentX + 1, true);
CPH.SetGlobalVar($"slider_matrix{currentX + 1}{currentY}", -1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY}", sceneItemId, true);
return true;
}
}slider_RightSubAction.cs
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
var sceneName = CPH.GetGlobalVar<string>("slider_SceneName", true);
var currentX = CPH.GetGlobalVar<int>("slider_currentX", true);
var currentY = CPH.GetGlobalVar<int>("slider_currentY", true);
var xCropDelta = CPH.GetGlobalVar<int>("slider_xCropDelta", true);
var yCropDelta = CPH.GetGlobalVar<int>("slider_yCropDelta", true);
if (currentX == 0)
{
return true;
}
var sceneItemId = CPH.GetGlobalVar<int>($"slider_matrix{currentX - 1}{currentY}", true);
CPH.LogDebug($"{sceneName} - {currentX} - {currentY} - {xCropDelta} - {yCropDelta} - {sceneItemId}");
var itemTransformResp = CPH.ObsSendRaw("GetSceneItemTransform", $"{{\"sceneName\":\"{sceneName}\",\"sceneItemId\":{sceneItemId}}}", 0);
JObject originalTransform = JObject.Parse(itemTransformResp);
originalTransform["sceneItemTransform"]["positionX"] = (int)originalTransform["sceneItemTransform"]["positionX"] + (xCropDelta * (double)originalTransform["sceneItemTransform"]["scaleX"]);
originalTransform["sceneItemTransform"]["boundsWidth"] = 1;
originalTransform["sceneItemTransform"]["boundsHeight"] = 1;
//
originalTransform.Add("sceneName", sceneName);
originalTransform.Add("sceneItemId", sceneItemId);
var tmpJson = originalTransform.ToString();
CPH.LogDebug(tmpJson);
CPH.ObsSendRaw("SetSceneItemTransform", tmpJson, 0);
//Write new state in global vars
CPH.SetGlobalVar("slider_currentX", currentX - 1, true);
CPH.SetGlobalVar($"slider_matrix{currentX - 1}{currentY}", -1, true);
CPH.SetGlobalVar($"slider_matrix{currentX}{currentY}", sceneItemId, true);
return true;
}
}Reasonably Asked Questions
Why?
Will this break if I resize parts of it, or the parent?
Are global variables the correct place to hold this information?
Can this be made to have smooth animations?
All of the movement functions are essentially the same but with additions and subtractions moved around, surely that can be condensed?