Unity3D 交互式AI桌面宠物开发系列【一】
文章主要介绍怎么制作AI桌面宠物的流程,我会从项目开始创建初期到最终可以和AI宠物进行交互为止,项目已经开发完成,我会仔细梳理一下流程,分步讲解。 这篇文章主要讲初期一些设置和部署。
提示:内容纯个人编写,欢迎评论点赞,来指正我。
文章目录
前言
本篇内容主要讲Unity开发桌面宠物前期准备工作,大家感兴趣也可以了解一下这个开发方向,目前还是挺有前景的,AI智能科技发展这么迅猛,紧跟步伐哈~
下面让我们出发吧 ------------>----------------->
一、AI桌面宠物是什么?
顾名思义,桌面宠物就是在PC端桌面上运行的一个2D/3D定制的虚拟形象,那么加上AI功能后就可以实现
宠物唤醒 ->识别用户语音->接入交互大模型->宠物语音播放内容->模型口型同步语音
当然后期还可以扩展一些类似和宠物直接的鼠标交互,比如点击一下播放个挠痒动画啊,或者向你招个手啊,后期我会根据反馈更新功能。
先看效果 跟我的拯救者毫无违和感, 哈哈哈哈哈
二、Unity 基础配置介绍
1.Unity版本
- 我开发使用的Unity版本是2021.3.44f1 目前没有遇到版本问题,当然高版本更好,最好不要太低,在2019.4以上吧尽量
2.形象配置
- 找一个3D模型,这个可爱的小姐姐是官方插件里面的,要找带有BlendShape组件的模型哦,网上很多的,找不到的跟我说。
三、开发功能
1.分辨率设置
- 分辨率设置,也就是窗口大小,在窗口内是鼠标是可以拖动着程序移动的。不要设置全屏,我设置的是 600*600
2. Player设置
- 按我这个设置就行,就不过多解释了,目的就是不影响窗口化
3. 窗口透明化、背景剔除效果
①. 相机设置
- 设置Clear Flags为Solid Color模式,然后将Background的颜色设置为黑色,透明度设置为0,其他默认或者自行调节,重点在下面的脚本
②. 代码剔除效果
TransparentWindow脚本代码:
using System;
using System.Runtime.InteropServices;
using UnityEngine;
public class TransparentWindow : MonoBehaviour
{
[SerializeField]
private Material m_Material;
private struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
[DllImport("Dwmapi.dll")]
private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);
const int GWL_STYLE = -16;
const uint WS_POPUP = 0x80000000;
const uint WS_VISIBLE = 0x10000000;
void Start()
{
#if !UNITY_EDITOR
var margins = new MARGINS() { cxLeftWidth = -1 };
var hwnd = GetActiveWindow();
SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
DwmExtendFrameIntoClientArea(hwnd, ref margins);
#endif
}
void OnRenderImage(RenderTexture from, RenderTexture to)
{
Graphics.Blit(from, to, m_Material);
}
}
- 挂载在相机上吗,下面是编写Shader效果,并制作材质球,挂载在这个脚本的Material变量处
③. Shader编写、材质球制作
Shader代码如下:
Shader "Custom/ChromakeyTransparent" {
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_TransparentColourKey("Transparent Colour Key", Color) = (0,0,0,1)
_TransparencyTolerance("Transparency Tolerance", Float) = 0.01
}
SubShader{
Pass{
Tags{ "RenderType" = "Opaque" }
LOD 200
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct a2v
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(a2v input)
{
v2f output;
output.pos = UnityObjectToClipPos(input.pos);
output.uv = input.uv;
return output;
}
sampler2D _MainTex;
float3 _TransparentColourKey;
float _TransparencyTolerance;
float4 frag(v2f input) : SV_Target
{
float4 colour = tex2D(_MainTex, input.uv);
from the chosen transparency colour
float deltaR = abs(colour.r - _TransparentColourKey.r);
float deltaG = abs(colour.g - _TransparentColourKey.g);
float deltaB = abs(colour.b - _TransparentColourKey.b);
// If colour is within tolerance, write a transparent pixel
if (deltaR < _TransparencyTolerance && deltaG < _TransparencyTolerance && deltaB < _TransparencyTolerance)
{
return float4(0.0f, 0.0f, 0.0f, 0.0f);
}
return colour;
}
ENDCG
}
}
}
- 新建Shader将上述代码贴进去,然后新建一个材质球,选择该Shader 如下图:
- 调整材质球的 Transparent Colour Key 组件的 颜色为黑色并将透明度Alpha值设置为0, 将制作好的材质球挂载在相机脚本 TransparentWindow 的材质球变量上就OK了。
- 到这一步就完成了透明背景!!!!接下来设置屏幕无边框
4. 程序无边框
可以参考 Unity打包窗口化放大、缩小、拖拽功能、无边框设置 C# 讲的也很详细,下面就再来一遍吧,为了项目完整性,换一种方式来。
① 无边框设置代码
using UnityEngine;
using System.Collections;
using System;
using System.Runtime.InteropServices;
using System.IO;
using UnityEngine.XR;
using UnityEngine.UI;
/// <summary>
/// 一共可选择三种样式
/// </summary>
public enum enumWinStyle
{
/// <summary>
/// 置顶
/// </summary>
WinTop,
/// <summary>
/// 置顶并且透明
/// </summary>
WinTopApha,
/// <summary>
/// 置顶透明并且可以穿透
/// </summary>
WinTopAphaPenetrate
}
public class WinSetting : MonoBehaviour
{
#region Win函数常量
private struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[DllImport("user32.dll")]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);
[DllImport("user32.dll")]
static extern int SetLayeredWindowAttributes(IntPtr hwnd, int crKey, int bAlpha, int dwFlags);
[DllImport("Dwmapi.dll")]
static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
private const int WS_POPUP = 0x800000;
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
private const int GWL_EXSTYLE = -20;
private const int GWL_STYLE = -16;
private const int WS_EX_LAYERED = 0x00080000;
private const int WS_BORDER = 0x00800000;
private const int WS_CAPTION = 0x00C00000;
const uint WS_VISIBLE = 0x10000000;
private const int SWP_SHOWWINDOW = 0x0040;
private const int LWA_COLORKEY = 0x00000001;
private const int LWA_ALPHA = 0x00000002;
private const int WS_EX_TRANSPARENT = 0x20;
//
private const int ULW_COLORKEY = 0x00000001;
private const int ULW_ALPHA = 0x00000002;
private const int ULW_OPAQUE = 0x00000004;
private const int ULW_EX_NORESIZE = 0x00000008;
#endregion
public string strProduct;//项目名称
public enumWinStyle WinStyle = enumWinStyle.WinTop;//窗体样式
public int ResWidth;//窗口宽度
public int ResHeight;//窗口高度
private int currentX;//窗口左上角坐标x
private int currentY;//窗口左上角坐标y
private bool isApha;//是否透明
private bool isAphaPenetrate;//是否要穿透窗体
void Awake()
{
Invoke("SetScreen", 0.2f);
}
void SetScreen()
{
currentX = Screen.width/2+300;
currentY = Screen.height/2;
Screen.fullScreen = false;
#if UNITY_EDITOR
print("编辑模式不更改窗体");
#else
switch (WinStyle)
{
case enumWinStyle.WinTop:
isApha = false;
isAphaPenetrate = false;
break;
case enumWinStyle.WinTopApha:
isApha = true;
isAphaPenetrate = false;
break;
case enumWinStyle.WinTopAphaPenetrate:
isApha = true;
isAphaPenetrate = true;
break;
}
IntPtr hwnd = FindWindow(null, strProduct);
if (isApha)
{
//去边框并且透明
SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED);
// 获得当前样式
int intExTemp = GetWindowLong(hwnd, GWL_EXSTYLE);
if (isAphaPenetrate)//是否透明穿透窗体
{
SetWindowLong(hwnd, GWL_EXSTYLE, intExTemp | WS_EX_TRANSPARENT | WS_EX_LAYERED);
}
SetWindowPos(hwnd, -1, currentX, currentY, ResWidth, ResHeight, SWP_SHOWWINDOW);
var margins = new MARGINS() { cxLeftWidth = -1 };
SetLayeredWindowAttributes(hwnd, 0, 255, 1);
}
else
{
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_BORDER & ~WS_CAPTION); //无边框、无标题栏
SetWindowPos(hwnd, -1, currentX, currentY, ResWidth, ResHeight, SWP_SHOWWINDOW);
}
#endif
}
/// <summary>
/// 拖拽程序
/// </summary>
public void Drag()
{
PInvoke.DragWindow();
Debug.Log("Drag");
}
}
-Drag函数里面的方法在如下脚本:
using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
public static class PInvoke
{
static IntPtr ptr;
public static IntPtr UnityHWnd
{
get
{
if (ptr == null || ptr == IntPtr.Zero)
{
ptr = GetUnityWindow();
}
return ptr;
}
}
#region 常量
public const ulong WS_MAXIMIZEBOX = 0x00010000L; //最大化的按钮禁用
public const ulong WS_DLGFRAME = 0x00400000L; //不现实边框
public const ulong WS_SIZEBOX = 0x00040000L; //调大小的边框
public const ulong WS_BORDER = 0x00800000L; //边框
public const ulong WS_CAPTION = 0x00C00000L; //标题栏
// Retreives pointer to WindowProc function.
public const int GWLP_WNDPROC = -4; //Windows 绘制方法的指针
public const int WM_SIZING = 0x214;
public const int WS_POPUP = 0x800000;
public const int GWL_STYLE = -16;
//边框参数
public const uint SWP_SHOWWINDOW = 0x0040;
public const uint SWP_NOMOVE = 0x0002;
public const int SW_SHOWMINIMIZED = 2;//(最小化窗口)
public const string UNITY_WND_CLASSNAME = "UnityWndClass";
#endregion
#region Win32 API
// Passes message information to the specified window procedure.
[DllImport("user32.dll")]
public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
//获得窗口样式
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
public static extern IntPtr GetWindowLongPtr(IntPtr hwnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowRect(IntPtr hwnd, ref RECT lpRect);
[DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);
// 改变指定窗口的属性 ,该函数还在额外窗口内存中的指定偏移处设置一个值。
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)]
public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
//设置当前窗口的显示状态
[DllImport("user32.dll")]
public static extern bool ShowWindow(System.IntPtr hwnd, int nCmdShow);
//设置窗口位置,大小
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
// 通过将每个窗口的句柄依次传递给应用程序定义的回调函数,枚举与线程关联的所有非子窗口。
[DllImport("user32.dll")]
private static extern bool EnumThreadWindows(uint dwThreadId, EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
//检索调用线程的线程标识符。
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
// 检索指定窗口所属的类的名称。
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern bool SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
#endregion
#region Static Function
//最小化窗口
public static void SetMinWindows()
{
if (!Application.isEditor)
{
ShowWindow(UnityHWnd, SW_SHOWMINIMIZED);
}
}
//拖动窗口
public static void DragWindow()
{
ReleaseCapture();
SendMessage(UnityHWnd, 0xA1, 0x02, 0);
SendMessage(UnityHWnd, 0x0202, 0, 0);
}
public static IntPtr GetUnityWindow()
{
var unityHWnd = IntPtr.Zero;
EnumThreadWindows(GetCurrentThreadId(), (hWnd, lParam) =>
{
var classText = new StringBuilder(UNITY_WND_CLASSNAME.Length + 1);
GetClassName(hWnd, classText, classText.Capacity);
if (classText.ToString() == UNITY_WND_CLASSNAME)
{
unityHWnd = hWnd;
return false;
}
return true;
}, IntPtr.Zero);
return unityHWnd;
}
#endregion
#region Assistant
/// <summary>
/// WinAPI RECT definition.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public override string ToString()
{
return "left = {Left}\nright = {Right}\ntop = {Top}\nbottom = {Bottom}";
}
}
#endregion
}
-将上述脚本挂载在相机或者其他物体上 如下WinSetting脚本:
StrProduct 变量替换成自己的项目的名字 去Player里面找
Win Style 里面有三种模式 默认第一种就好 其他两种可以自行测试效果
② 增加程序拖拽功能
- 可以在分辨率大小内的窗口里鼠标左键按住拖拽窗口,这个范围是设置的分辨率大小,自己根据需要设置,目前是600*600的大小
- 新建一个Image
!!!!!重点!!!!!!!!!!!!!
- (1)Image的锚点设置为充满全屏也就是大小跟分辨率一样,这样才能保证分辨率内的任何地方都可以拖拽程序
- (2) Image的颜色设置随便,Alpha透明度设置为0 ,达到看不见UI的效果。
- (3)添加组件EventTrigger ,然后添加一个Drag事件,将相机上的WinSetting方法绑定在Drag事件上。
4.去Logo 直接上代码
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Scripting;
#if !UNITY_EDITOR
[Preserve] //防止打包的时候没有被打包进程序
public class ClearLogoAni : MonoBehaviour
{
//在启动画面显示之前执行这个方法
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void Run()
{
Task.Run(() =>
{
SplashScreen.Stop(SplashScreen.StopBehavior.StopImmediate);
});
}
}
#endif
- 直接扔哪个代码文件里,不需要挂载任何物体,运行时自动执行!!
然后保存设置并打包测试吧
小姐姐给大家作揖啦
比心啦 ❥(^_-)
桌面宠物基础功能就实现啦!
四、总结
- 提示: 大家根据需求来做功能,后续继续其他功能啦,不懂的快喊我。
- 大家可以在评论区讨论其他系列下一期出什么内容,这个系列会继续更新的
- 点赞收藏加关注哦~ 蟹蟹