Unity3D and Linux support via Wine

I wrote this article for the Seven Day FPS challenge originally.

Hallo!

I guess a lot of you will use Unity3D, as it is a nice tool and making an FPS with it is pretty easy. I would use it, too, if I would have time to participate. Nonetheless, here is a little gem of wisdom I'd like to share: Unity works in Linux via Wine, but not perfectly. If you don't need mouse movement, I guess you're fine, but as soon as you want that funky look around camera control via mouse you love in your modern FPSes, the bad news is that under Wine the following code (C#) doesn't do what you expect:

transform.RotateAround(Vector3.up, Input.GetAxis("Mouse X") * horizontalSpeed * sensitivity);

It won't rotate, as Input.GetAxis("Mouse X") (and "Mouse Y", too) will always return 0. Okay, you say, perhaps I just want to use Input.mousePosition and calculate the delta between the mouse position from last frame to this frame? Well, that works, but only if you don't use Screen.lockCursor = true - which unfortunately is the only way in Unity to reposition the cursor (it transfers it to the center of the game window every frame). You could also use Screen.showCursor = false, but of course it has the problem that the cursor sooner or later will drop out of the window or hit the screen's borders.

So, what is the solution here? The one I found lies in this forum thread (Unity3D support forum) - but if you know a better and simpler one, please share! Anyway, this is what's going on:

  • Get the mouse position via the user32.dll (so be careful to only use this code for the Windows version - use #if UNITY_STANDALONE_WIN for that).
  • Get the delta mouse movement by subtracting the last mouse position from the screen center.
  • Reposition the cursor to the screen  center via user32.dll.
  • Hide the mouse cursor via Screen.showCursor = false.

One little problem with this approach is the fact that I didn't find a way to get the exact center of the game window in the Unity editor (because you can move it around), so sometimes the "center" is outside of this window, but I guess you can circumvent that by using the normal Input.GetAxis("Mouse X") in the editor. Nonetheless, here is the code I use:

First, the WindowsCursorHandling.cs (most parts blindly copied from the aforementioned thread) - you have to add that component to a gameobject in the scene.

using UnityEngine;

#if UNITY_STANDALONE_WIN
using System.Collections;
using System.Runtime.InteropServices;

public class WindowsCursorHandling : MonoBehaviour
{
 [DllImport( "user32.dll", CharSet = CharSet.Auto, ExactSpelling = true )]
 [return: MarshalAs( UnmanagedType.Bool )]
 public static extern bool ClipCursor( ref RECT rcClip );
 [DllImport( "user32.dll" )]
 [return: MarshalAs( UnmanagedType.Bool )]
 public static extern bool GetClipCursor( out RECT rcClip );
 [DllImport( "user32.dll" )]
 static extern int GetForegroundWindow( );
 [DllImport("user32.dll")]
 [return: MarshalAs( UnmanagedType.Bool )]
 static extern bool GetWindowRect( int hWnd, ref RECT lpRect );
 [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
 public static extern int SetCursorPos(int x, int y);
 [DllImport("user32.dll")]
 [return: MarshalAs( UnmanagedType.Bool )]
 public static extern bool GetCursorPos(out POINT point);

 private static RECT currentClippingRect;
 private static RECT originalClippingRect = new RECT( );
 private static bool isClipped = false;

 public static int centerX;
 public static int centerY;

 [StructLayout( LayoutKind.Sequential )]
 public struct RECT
 {
  public int Left;
  public int Top;
  public int Right;
  public int Bottom;
  public RECT( int left, int top, int right, int bottom )
  {
   Left = left;
   Top = top;
   Right = right;
   Bottom = bottom;
  }
 }

 [StructLayout( LayoutKind.Sequential )]
 public struct POINT
 {
  public int x;
  public int y;
  public POINT( int X, int Y )
  {
   x = X;
   y = Y;
  }
 }

 void Start()
 {
  GetClipCursor( out originalClippingRect );
  centerX = originalClippingRect.Left + (originalClippingRect.Right - originalClippingRect.Left) / 2;
  centerY = originalClippingRect.Top + (originalClippingRect.Bottom - originalClippingRect.Top) / 2;
 }

 public static void StartClipping()
 {
  if (isClipped)
   return;

  var hndl = GetForegroundWindow( );
  GetWindowRect( hndl, ref currentClippingRect );
  ClipCursor( ref currentClippingRect);

#if UNITY_EDITOR
  centerX = Screen.width / 2;
  centerY = Screen.height / 2;
#else
  centerX = currentClippingRect.Left + (currentClippingRect.Right - currentClippingRect.Left) / 2;
  centerY = currentClippingRect.Top + (currentClippingRect.Bottom - currentClippingRect.Top) / 2;
#endif
  isClipped = true;
 }

 static public void CenterCursor()
 {
  SetCursorPos(centerX, centerY);
 }

 void OnApplicationQuit()
 {
  StopClipping();
 }
 public static void StopClipping()
 {
  isClipped = false;
  ClipCursor(ref originalClippingRect);
 }
}
#else
public class WindowsCursorHandling : MonoBehaviour
{
}
#endif

And this is how it then looks in my Camera Controller for FPS mouse movement:

#if UNITY_STANDALONE_WIN
 WindowsCursorHandling.POINT p;
 WindowsCursorHandling.GetCursorPos(out p);
 float factorX = 40.0f * (p.x - WindowsCursorHandling.centerX) / (float)Screen.width;
 float factorY = -40.0f * (p.y - WindowsCursorHandling.centerY) / (float)Screen.width;
 WindowsCursorHandling.CenterCursor();
 transform.RotateAround(Vector3.up, factorX * horizontalSpeed);
#else
 transform.RotateAround(Vector3.up, Input.GetAxis("Mouse X") * horizontalSpeed);
#endif
// do similar stuff with "Mouse Y" movement

The magic number 40.0f was found by trial-and-error, so I don't know if it's really the best value for every PC configuration. You still need the cursor hiding stuff somewhere, and of course call WindowsCursorHandling.StartClipping() at initialization. Whenever I wanted to just call Screen.lockCursor I now call this function instead:

static public void SetCursorHidden(bool hide)
{
#if UNITY_STANDALONE_WIN
 Screen.showCursor = !hide;
 if (hide)
  WindowsCursorHandling.StartClipping();
 else
  WindowsCursorHandling.StopClipping();
#else
 Screen.lockCursor = hide;
#endif
}

That's it!