Unity+Android Studio: 桌面宠物 背景透明 解决方案_发现流程_信息汇总

前言

首次尝试unity项目的安卓移植,核心需求就是在其他应用之上以浮窗(小窗口?)显示,同时保证只显示宠物,背景透明(就像桌宠理所当然的样子😐)。开发过程中尝试参考了众多CSDN和Unity Discussions的内容,最终及大家之所长找到了解决方案(详见参考链接)。

目录

前言

Unity设置

Unity设置-网上说的其他有用操作

导出Android项目

Android Studio项目配置及修改

咋还不行啊(发现流程)

实验

测试surfaceView在自定义Layout上的行为

模拟UnityPlayer的嵌套结构在自定义Layout上的行为

设置View的Alpha值

解决方案

总结

参考,来源,引用


Unity设置

让背景透明唯一要做的就是将Camera中的 Camera➡️Clear Flags设置为Solid Color, Camera➡️Background 中的RGBA全部拉到0。结束!

Unity设置-网上说的其他有用操作

1.使用有8位颜色值的老版本Unity:未尝试(移植项目,不敢换版本)✖

2.将Project Settings➡️Player➡️安卓tab➡️Resolution and Presentation➡️preserve Alpha Channel 启用:笔者使用的Unity2022.3.0版本没找到这个选项 ✖

3. 将Project Settings➡️Player➡️安卓tab➡️Resolution and Presentation➡️Render Over Native UI启用:点不点这个都没区别 ✖

结论:都没有用(或不能用)

导出Android项目

如果大家还会在Unity中对项目进行修改的话,建议使用一些版本控制工具,因为每次重新导出项目Unity都会会覆盖AndroidMainfest配置文件,而我们需要修改这个文件来实现效果。

Android Studio项目配置及修改

需要修改UnityLibrary中的AndroidMainfest,res➡️layout中添加新的layout,此layout需要包含一个子layout,修改gradle文件等。(详见参考1)

将UnityPlayerActivity中控制mUnityPlayer的语句注释掉,新建一个Service类控制其生命周期,新建继承自UnityPlayerActivity的自定义Activity在获得显示在其他应用权限后启动Service的代码。在AndroidMainfest的Activity 那么替换为我们的自定义Activity来启动项目,添加自定义Service。(详见参考1)

咋还不行啊(发现流程)

尝试了上述方案,网上说的Unity所有其他有用操作,和AI生成的的诸多方案后程序仍然运行在黑色背景的小窗口上,笔者非常绝望,这时看到了参考3提到的UnityPlayer图层关系和源码,发现了以下关系:

class UnityPlayer extends FrameLayout{
private J mGlView;
...
}


class J extends FrameLayout {
private a a;
...
}


class a extends SurfaceView {
...
}

所以UnityPlayer是FrameLayout套着FrameLayout套着SurfaceView这样三层View嵌套的关系。

及这样的关系

我们Unity的画面是渲染在最上层的SurfaceView a上的。到这一步经过某些尝试可以直接试出我们的解决方案,不想看发现流程的朋友们可以直接移步解决方案👍。

实验

首先笔者不确定是导出后的安卓项目还是Unity渲染的原因导致的黑色背景,于是创建了一个自定义的SurfaceView class来显示一些随随便便的画面并测试其添加到我们layout上的行为:

package com.unity3d.player;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;


public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    private SurfaceHolder surfaceHolder;
    private Paint paint;
    private DrawingThread drawingThread;

    public TestSurfaceView(Context context) {
        super(context);
        init();
    }

    public TestSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        paint = new Paint();
        paint.setColor(Color.RED); // Example color for custom drawing
        paint.setTextSize(50); // Example text size for custom drawing
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        drawingThread = new DrawingThread(holder);
        drawingThread.setRunning(true);
        drawingThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Handle surface changes if needed
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;
        drawingThread.setRunning(false);
        while (retry) {
            try {
                drawingThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // Retry shutting down the thread
            }
        }
    }

    private class DrawingThread extends Thread {
        private SurfaceHolder surfaceHolder;
        private boolean running = false;

        public DrawingThread(SurfaceHolder surfaceHolder) {
            this.surfaceHolder = surfaceHolder;
        }

        public void setRunning(boolean running) {
            this.running = running;
        }

        @Override
        public void run() {
            while (running) {
                Canvas canvas = null;
                try {
                    canvas = surfaceHolder.lockCanvas();
                    if (canvas != null) {
                        synchronized (surfaceHolder) {
                            drawRandomPattern(canvas);
                        }
                    }
                } finally {
                    if (canvas != null) {
                        surfaceHolder.unlockCanvasAndPost(canvas);
                    }
                }
            }
        }

        private void drawRandomPattern(Canvas canvas) {
            canvas.drawColor(Color.TRANSPARENT);
            // Draw a fixed pattern: for example, a grid of circles
            paint.setColor(Color.RED);
            int radius = 50;
            int padding = 20;
            int columns = (canvas.getWidth() - padding) / (2 * radius + padding);
            int rows = (canvas.getHeight() - padding) / (2 * radius + padding);

            for (int i = 0; i < columns; i++) {
                for (int j = 0; j < rows; j++) {
                    float x = padding + radius + i * (2 * radius + padding);
                    float y = padding + radius + j * (2 * radius + padding);
                    canvas.drawCircle(x, y, radius, paint);
                }
            }

            // Draw some text in the center
            paint.setColor(Color.WHITE);
            paint.setTextSize(50);
            canvas.drawText("Hello, SurfaceView!", 50, canvas.getHeight() / 2, paint);
        }
    }
}

注意drawRandomPattern这个方法第一行首先把把整个图渲染为透明,然后在透明画面上渲染

测试surfaceView在自定义Layout上的行为

在自定义Service中把我们的TestSurfaceView 放到自定义layout上:

TestSurfaceView t = new TestSurfaceView(this);
((RelativeLayout) customizedLayout.findViewById(R.id.overlay_view)).addView(t);

得到以下画面:

背景还是黑色的,是我们的自定义layout出了问题吗?这时看到了 参考2 的信息,加入此行代码:

TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);//关键
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(t);

 得到以下画面:

所以surfaceView默认是使用没有alpha通道的像素格式渲染的,需要通过这行代码设置为有alpha通道的像素格式。同时证明了我们自定义layout的可行性!😆

在自定义service中,取出UnityPlayer的surfaceView并设置像素格式:

mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);

运行,没有任何变化。。。😔 这时候已经开始怀疑是unity渲染时输出的像素值就是不透明的。遂作出以下实验:将TestSurfaceView➡️drawRandomPattern 第一行改为Color.BLACK(保留setFormat(PixelFormat.TRANSPARENT)),得到以下画面:

这说明透明背景的首要前提就是 渲染过程中真的在输出alpha为0的像素(好像是废话🥲)。

OK!得到以上结论后,通过实验判断是否Unity在输出正确的像素!!

模拟UnityPlayer的嵌套结构在自定义Layout上的行为

创建一个想UnityPlayer一样的View嵌套结构,最上层为我们的TestSurfaceView,并把这个嵌套View添加到自定义layout上运行:

FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);

最后一行 t.setZOrderOnTop(true) 是必要的,因为SurfaceView会创建一个单独的绘图层,它可能被放置在与其他View结构不同的窗口层中;所以虽然它是其他View的child,它却有可能被放置在其他View的上层或下层,导致意料之外的Z轴问题。所以我们使用这个方法把SurfaceView 放置在Z轴最上层,也就是所有View之上,如果查看UnityPlayer的源码我们会发现J类在创建a类对象时也使用了这个方法。

设置View的Alpha值

这个方法是我在询问AI相关解决方案时发现的,View.setAlpha(float)理论上会设置一个View和他所有的子View的透明度,可以让窗口变成半透明显示(可以把桌宠放到刚刚好的窗口里然后给整个View设置一个合适的Alpha来做一个缓兵之计😂)。

Unity View设置Alpha:

mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
mUnityPlayer.setAlpha(0.5f);
mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
surfaceView.setAlpha(0.5f);

设置mUnityPlayer的alpha,整个窗口变成半透明的;

设置surfaceView的alpha,窗口中背景为不透明的黑色,但是桌宠变成半透明了。(无法展示开发中画面,向大家致歉)

模拟Unity结构的View设置Alpha:

我们要对比Unity的行为与渲染背景为不透明像素的行为,所以TestSurfaceView➡️ drawRandomPattern 第一行保持为Color.BLACK

FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);
uniPlayerSimu.setAlpha(0.5f);
FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);
t.setAlpha(0.5f);

分别得到以下结果(图左是uniPlayerSimu.setAlpha;图右是t.setAlpha):

重点分析右图,通过设置渲染不透明像素的SurfaceView的Alpha值,整个窗口都变成半透明了;而Unity设置SurfaceView的Alpha,只有桌宠的透明度改变了,背景透明度并没有变化,这说明背景的黑色并不是Unity的SurfaceView渲染的!!!(不知道为啥左图没有变成半透明的😑😑😑)

解决方案

通过以上的实验,笔者发现了黑色背景并非UnityPlayer中的SurfaceView渲染的,那么其一定是其他某个View渲染的,而我们已经知道了UnityPlayer中的嵌套结构,只要尝试将背景颜色设为透明就可以解决这个问题。

最终发现是二层J类对象背景颜色为黑色,将其设计为透明:

mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
FrameLayout glView = (FrameLayout)mUnityPlayer.getChildAt(0);
glView.setBackgroundColor(Color.TRANSPARENT);
//下面两行删了也没有影响,但是是探索得到的知识,所以写在这里
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);

运行,解决🎉🎉🎉!!!

总结

1. SurfaceHolder默认使用没有alpha通道的颜色格式,要想输出透明的颜色,必须要使用surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); 当然UnityPlayer已经设置了。

2. 如果想让SurfaceView显示在正确的View层级上(一般来说就是使其放在z轴最上方),需要使用t.setZOrderOnTop(true);因为当创建一个SurfaceView并把它添加为其他View的child时,它会意料之外的位于其他View的上面或者下面。

如果SurfaceView放置于了一个View下方(View的z轴位置高于SurfaceView,View盖住了SurfaceView,View是SurfaceView的上层),这会反直觉地让上层View无法正确显示:

因为SurfaceView被放置在了View结构之外的窗口层(window layer)中来保证它会被显示,正如它的名字Surface表面视图一样,保证了SurfaceView的显示,所以导致了行为上其上层的View被丢弃/隐藏了。当然UnityPlayer中也调用了这个方法。

3. View.setAlpha()会设置一个View和其所有子View的透明度,所以当Unity最上层View mUnityPlayer 设置透明时整个窗口都透明化了,这个方法可以被当作一个权宜之计。至于为什么我模拟Unity嵌套结构的自定义View设置时没有发生同样的结果,希望有大佬来解答。

最后,以上是通过实验得出的结论,但笔者在写文章时又多看了两行源码,发现J类构造器中把背景颜色设置为了黑色。。。😐😑😓

this.setBackgroundColor(-16777216);

笔者是第一次开发Android项目,对相关原理和专有词汇不太熟悉,可能多有谬误,还望大家多多指正,指教。感谢!

参考,来源,引用

1. 用service控制UnityPlayer Activity生命周期以及使用layout让程序以浮窗(小窗口?)显示:

Unity+AndroidStudio混合开发:安卓手机桌面宠物_unity 桌面宠物-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_52847003/article/details/131656114

2. 让SurfaceView背景透明;把自定义View放置于UnityPlayer View下方:

Unity Android 背景透明 相机透明 - 简书 (jianshu.com)icon-default.png?t=N7T8https://www.jianshu.com/p/a67f77cd2e623. 提醒大家Unity图层关系以及查看源码:

Unity导出Android制作桌面宠物精灵需要背景透明_unity实现桌面宠物-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_40897966/article/details/109675667?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-109675667-blog-131656114.235%5Ev43%5Epc_blog_bottom_relevance_base3&spm=1001.2101.3001.4242.1&utm_relevant_index=14. 后续悬浮窗开发:

Android悬浮窗看这篇就够了_安卓如何把变量 放到悬浮框-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_37750825/article/details/1157546475. Unity Disccusion中大家的热情的Discussion:

Unity3D export to Android with transparent background? - Unity Engine - Unity Discussionsicon-default.png?t=N7T8https://discussions.unity.com/t/unity3d-export-to-android-with-transparent-background/688050/52?page=2大家都是非常好的创作者,非常感谢你们的分享❤️

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值