Compare commits

..

16 Commits

Author SHA1 Message Date
mob-sakai
0441238cc5 docs: update readme 2026-06-24 15:10:37 +09:00
mob-sakai
639bc9c342 fix: rename UIParticleProjectSettings.enableLinearToGamma to autoColorCorrection 2026-06-24 15:10:37 +09:00
mob-sakai
b3d42a2478 fix: UI/Additive shader does not support RectMask2D softness. 2026-06-24 15:10:37 +09:00
mob-sakai
418b257ae2 fix: there is a compilation error in Unity 2019.1 or earlier
close #407
2026-06-24 15:10:36 +09:00
mob-sakai
664c7963ea chore: update internal 2026-06-24 15:10:36 +09:00
mob-sakai
7363f7497c feat: preview the ParticleSystem playback when selecting a UIParticle in the Editor 2026-06-24 15:10:36 +09:00
mob-sakai
f961b8dc89 chore: fix workflow for pwn request 2026-06-12 16:27:44 +09:00
semantic-release-bot
18175c040e chore(release): 4.12.2 [skip ci]
## [4.12.2](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.12.1...v4.12.2) (2026-06-08)

### Bug Fixes

* add `meshCleared` flag to optimize mesh clearing ([859fa20](859fa20d29))
* fix Unity6.5 compile errors and warnings ([a5ee687](a5ee687821)), closes [#400](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/400)
* potential access to UIParticleRenderer that has already been destroyed ([b740dd6](b740dd662d)), closes [#403](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/403)
* updated support for some changed menu paths ([f8ac986](f8ac9869f1)), closes [#397](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/397)
2026-06-08 13:33:41 +00:00
mob-sakai
9cd47c32bc chore: ignore alpha for test 2026-06-08 22:33:03 +09:00
mob-sakai
48b38ec34f update internal code 2026-06-08 15:55:42 +09:00
mob-sakai
b740dd662d fix: potential access to UIParticleRenderer that has already been destroyed
close #403
2026-06-08 13:28:24 +09:00
tako
f8ac9869f1 fix: updated support for some changed menu paths
close #397
2026-06-08 13:28:24 +09:00
tako
a5ee687821 fix: fix Unity6.5 compile errors and warnings
close #400
2026-06-08 13:28:24 +09:00
Minhyuk Kim
859fa20d29 fix: add meshCleared flag to optimize mesh clearing 2026-06-08 11:33:57 +09:00
semantic-release-bot
d89d394e04 chore(release): 4.12.1 [skip ci]
## [4.12.1](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.12.0...v4.12.1) (2026-03-24)

### Bug Fixes

* ignore "EditorOnly" tagged gameObjects on refresh ([031d46a](031d46a321))
2026-03-24 08:53:33 +00:00
mob-sakai
031d46a321 fix: ignore "EditorOnly" tagged gameObjects on refresh 2026-03-24 17:53:01 +09:00
36 changed files with 807 additions and 129 deletions

6
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,6 @@
# This is a comment.
# Each line is a file pattern followed by one or more owners.
# https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default owners
* @mob-sakai

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: mob-sakai # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: mob_sakai # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: mob-sakai
---
NOTE: Your issue may already be reported! Please search on the [issue tracker](../) before creating one.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- Version [e.g. 1.0.0]
- Platform: [e.g. Editor(Windows/Mac), Standalone(Windows/Mac), iOS, Android, WebGL]
- Unity version: [e.g. 2018.2.8f1]
- Build options: [e.g. IL2CPP, .Net 4.x, LWRP]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: mob-sakai
---
NOTE: Your issue may already be reported! Please search on the [issue tracker](../) before creating one.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: Ask a question about this project
title: ''
labels: question
assignees: mob-sakai
---
NOTE: Your issue may already be reported! Please search on the [issue tracker](../) before creating one.
**Describe what help do you need**
A description of the question.
**Additional context**
Add any other context or screenshots about the question here.

37
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,37 @@
# Pull Request Template
## Description
- Please include a summary of the change and which issue is fixed.
- Please also include relevant motivation and context.
- List any dependencies that are required for this change.
Fixes #{issue_number}
## Type of change
Please write the commit message in the format corresponding to the change type.
Please see [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for more information.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Update documentations
- [ ] Others (refactoring, style changes, etc.)
## Test environment
- Platform: [e.g. Editor(Windows/Mac), Standalone(Windows/Mac), iOS, Android, WebGL]
- Unity version: [e.g. 2022.2.0f1]
- Build options: [e.g. IL2CPP, .Net 4.x, URP/HDRP]
## Checklist
- [ ] This pull request is for merging into the `develop` branch
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have checked my code and corrected any misspellings

View File

@@ -27,10 +27,10 @@ jobs:
split_to: ${{ steps.summary.outputs.split_to }} split_to: ${{ steps.summary.outputs.split_to }}
steps: steps:
- name: 🚚 Checkout (${{ github.ref_name }}) - name: 🚚 Checkout (${{ github.ref_name }})
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: 🔖 Run semantic release - name: 🔖 Run semantic release
uses: cycjimmy/semantic-release-action@v5 uses: cycjimmy/semantic-release-action@v6
id: release id: release
with: with:
working_directory: Packages/src working_directory: Packages/src
@@ -38,7 +38,7 @@ jobs:
@semantic-release/changelog @semantic-release/changelog
@semantic-release/git @semantic-release/git
env: env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} GITHUB_TOKEN: ${{ github.token }}
- id: summary - id: summary
run: | run: |

View File

@@ -7,9 +7,9 @@ run-name: 🧪 Test (${{ github.event.pull_request.title || github.ref_name }})
env: env:
# MINIMUM_VERSION: The minimum version of Unity. # MINIMUM_VERSION: The minimum version of Unity.
MINIMUM_VERSION: 2019.4 MINIMUM_VERSION: 2020.3
# EXCLUDE_FILTER: The excluded versions of Unity. # EXCLUDE_FILTER: The excluded versions of Unity.
EXCLUDE_FILTER: "(2020.2.0|2021.1|2023.3)" EXCLUDE_FILTER: "(2017|2018|2023.3)"
PROJECT_PATH: . PROJECT_PATH: .
on: on:
@@ -27,7 +27,7 @@ on:
- "!*" - "!*"
paths-ignore: paths-ignore:
- "**.md" - "**.md"
pull_request_target: pull_request:
types: types:
- opened - opened
- reopened - reopened
@@ -39,16 +39,29 @@ jobs:
setup: setup:
name: ⚙️ Setup name: ⚙️ Setup
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: {} # No permissions needed for setup job
outputs: outputs:
unityVersions: ${{ steps.setup.outputs.unityVersions }} unityVersions: ${{ steps.setup.outputs.unityVersions }}
steps: steps:
- name: 🔑 Secrets check
run: |
if [ -z "$UNITY_EMAIL" ] || [ -z "$UNITY_PASSWORD" ] || [ -z "UNITY_LICENSE" ]; then
echo "Error: UNITY_EMAIL, UNITY_PASSWORD, and UNITY_LICENSE secrets must be set." | tee -a $GITHUB_STEP_SUMMARY >&2
echo "Error: See https://game.ci/docs/github/test-runner#basic-setup" | tee -a $GITHUB_STEP_SUMMARY >&2
echo "Error: Set the secrets at ${{ github.server_url }}/${{ github.repository }}/settings/secrets/actions" | tee -a $GITHUB_STEP_SUMMARY >&2
exit 1
fi
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
- name: ⚙️ Find target Unity versions - name: ⚙️ Find target Unity versions
id: setup id: setup
run: | run: |
echo "==== Target Unity Versions ====" echo "==== Target Unity Versions ===="
LATEST_VERSIONS=`npx unity-changeset@latest list --versions --latest-patch --min ${MINIMUM_VERSION} --json --all` LATEST_VERSIONS=`npx -y unity-changeset@latest list --json --versions --all --latest-patch --ignore-alpha --min ${MINIMUM_VERSION}`
if [ "${{ inputs.usePeriodVersions }}" = "true" ]; then if [ "${{ inputs.usePeriodVersions }}" = "true" ]; then
ADDITIONAL_VERSIONS=`npx unity-changeset list --versions --grep '0f' --min ${MINIMUM_VERSION} --json` ADDITIONAL_VERSIONS=`npx -y unity-changeset@latest list --json --versions --ignore-alpha --min ${MINIMUM_VERSION} --grep '0f'`
else else
ADDITIONAL_VERSIONS=[] ADDITIONAL_VERSIONS=[]
fi fi
@@ -66,31 +79,15 @@ jobs:
needs: setup needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 6 max-parallel: 8
matrix: matrix:
unityVersion: ${{ fromJson(needs.setup.outputs.unityVersions) }} unityVersion: ${{ fromJson(needs.setup.outputs.unityVersions) }}
steps: steps:
- name: 🚚 Checkout ($${{ github.ref }}) - name: 🚚 Checkout ($${{ github.ref }})
if: github.event_name == 'push' uses: actions/checkout@v6
uses: actions/checkout@v5
- name: 🚚 Checkout pull request (pull_request_target)
if: github.event_name == 'pull_request_target'
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: 🚚 Marge pull request (pull_request_target)
if: github.event_name == 'pull_request_target'
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git rebase ${{ github.event.pull_request.base.sha }}
git log --oneline -n 10
- name: 📥 Cache library - name: 📥 Cache library
uses: actions/cache@v4 uses: actions/cache@v5
with: with:
path: ${{ env.PROJECT_PATH }}/Library path: ${{ env.PROJECT_PATH }}/Library
key: ${{ env.PROJECT_PATH }}-Library-${{ matrix.unityVersion }}-${{ github.event.pull_request.head.sha || github.sha }} key: ${{ env.PROJECT_PATH }}-Library-${{ matrix.unityVersion }}-${{ github.event.pull_request.head.sha || github.sha }}
@@ -99,7 +96,7 @@ jobs:
${{ env.PROJECT_PATH }}-Library- ${{ env.PROJECT_PATH }}-Library-
- name: 🛠️ Build Unity Project (Test) - name: 🛠️ Build Unity Project (Test)
uses: game-ci/unity-builder@main uses: game-ci/unity-builder@v5
timeout-minutes: 45 timeout-minutes: 45
with: with:
customImage: ghcr.io/mob-sakai/unity3d:${{ matrix.unityVersion }} customImage: ghcr.io/mob-sakai/unity3d:${{ matrix.unityVersion }}

View File

@@ -3,7 +3,7 @@
"com.coffee.development": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/Development", "com.coffee.development": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/Development",
"com.coffee.minimal-resource": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/MinimalResource", "com.coffee.minimal-resource": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/MinimalResource",
"com.coffee.nano-monitor": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/NanoMonitor", "com.coffee.nano-monitor": "https://github.com/mob-sakai/Coffee.Internal.git?path=Packages/NanoMonitor",
"com.unity.ide.rider": "3.0.31", "com.unity.ide.rider": "3.0.36",
"com.unity.test-framework": "1.1.33", "com.unity.test-framework": "1.1.33",
"com.unity.modules.animation": "1.0.0", "com.unity.modules.animation": "1.0.0",
"com.unity.modules.physics": "1.0.0" "com.unity.modules.physics": "1.0.0"

View File

@@ -40,7 +40,7 @@
"url": "https://packages.unity.com" "url": "https://packages.unity.com"
}, },
"com.unity.ide.rider": { "com.unity.ide.rider": {
"version": "3.0.31", "version": "3.0.36",
"depth": 0, "depth": 0,
"source": "registry", "source": "registry",
"dependencies": { "dependencies": {

View File

@@ -1,3 +1,20 @@
## [4.12.2](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.12.1...v4.12.2) (2026-06-08)
### Bug Fixes
* add `meshCleared` flag to optimize mesh clearing ([859fa20](https://github.com/mob-sakai/ParticleEffectForUGUI/commit/859fa20d297c3f44e3361f20dbb7ce966407e03e))
* fix Unity6.5 compile errors and warnings ([a5ee687](https://github.com/mob-sakai/ParticleEffectForUGUI/commit/a5ee6878212be2fc4d7b48879426f239e8753009)), closes [#400](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/400)
* potential access to UIParticleRenderer that has already been destroyed ([b740dd6](https://github.com/mob-sakai/ParticleEffectForUGUI/commit/b740dd662d423c6bef849662ce1b0bfbb4940ed4)), closes [#403](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/403)
* updated support for some changed menu paths ([f8ac986](https://github.com/mob-sakai/ParticleEffectForUGUI/commit/f8ac9869f141238169730e74f5d65c4fc6081f51)), closes [#397](https://github.com/mob-sakai/ParticleEffectForUGUI/issues/397)
## [4.12.1](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.12.0...v4.12.1) (2026-03-24)
### Bug Fixes
* ignore "EditorOnly" tagged gameObjects on refresh ([031d46a](https://github.com/mob-sakai/ParticleEffectForUGUI/commit/031d46a3216c942d2d1a6ccfadf5f0b9e3ce3006))
# [4.12.0](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.11.4...v4.12.0) (2026-03-24) # [4.12.0](https://github.com/mob-sakai/ParticleEffectForUGUI/compare/v4.11.4...v4.12.0) (2026-03-24)

View File

@@ -78,10 +78,19 @@ namespace Coffee.UIExtensions
var mat = mats[j]; var mat = mats[j];
if (mat == null || mat.shader == null) continue; if (mat == null || mat.shader == null) continue;
#if UNITY_6000_5_OR_NEWER
for (var i = 0; i < mat.shader.GetPropertyCount(); i++)
#else
for (var i = 0; i < ShaderUtil.GetPropertyCount(mat.shader); i++) for (var i = 0; i < ShaderUtil.GetPropertyCount(mat.shader); i++)
#endif
{ {
#if UNITY_6000_5_OR_NEWER
var name = mat.shader.GetPropertyName(i);
var type = (AnimatableProperty.ShaderPropertyType)mat.shader.GetPropertyType(i);
#else
var name = ShaderUtil.GetPropertyName(mat.shader, i); var name = ShaderUtil.GetPropertyName(mat.shader, i);
var type = (AnimatableProperty.ShaderPropertyType)ShaderUtil.GetPropertyType(mat.shader, i); var type = (AnimatableProperty.ShaderPropertyType)ShaderUtil.GetPropertyType(mat.shader, i);
#endif
if (!s_Names.Add(name)) continue; if (!s_Names.Add(name)) continue;
AddMenu(gm, sp, new ShaderProperty(name, type), true); AddMenu(gm, sp, new ShaderProperty(name, type), true);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -8,12 +9,11 @@ using UnityEngine;
using UnityEngine.Profiling; using UnityEngine.Profiling;
using UnityEngine.UI; using UnityEngine.UI;
using Coffee.UIParticleInternal; using Coffee.UIParticleInternal;
using Object = UnityEngine.Object;
#if UNITY_2021_2_OR_NEWER #if UNITY_2021_2_OR_NEWER
using UnityEditor.Overlays; using UnityEditor.Overlays;
#else #else
using System;
using System.Reflection; using System.Reflection;
using Object = UnityEngine.Object;
#endif #endif
#if UNITY_2021_2_OR_NEWER #if UNITY_2021_2_OR_NEWER
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
@@ -49,6 +49,7 @@ namespace Coffee.UIExtensions
private static readonly GUIContent s_ContentRandom = new GUIContent("Random"); private static readonly GUIContent s_ContentRandom = new GUIContent("Random");
private static readonly GUIContent s_ContentScale = new GUIContent("Scale"); private static readonly GUIContent s_ContentScale = new GUIContent("Scale");
private static readonly GUIContent s_ContentPrimary = new GUIContent("Primary"); private static readonly GUIContent s_ContentPrimary = new GUIContent("Primary");
private static readonly GUIContent s_Remove = new GUIContent("Remove");
private static readonly Regex s_RegexBuiltInGuid = new Regex(@"^0{16}.0{15}$", RegexOptions.Compiled); private static readonly Regex s_RegexBuiltInGuid = new Regex(@"^0{16}.0{15}$", RegexOptions.Compiled);
private static readonly List<Material> s_TempMaterials = new List<Material>(); private static readonly List<Material> s_TempMaterials = new List<Material>();
@@ -66,6 +67,7 @@ namespace Coffee.UIExtensions
private ReorderableList _ro; private ReorderableList _ro;
private bool _showMax; private bool _showMax;
private bool _is3DScaleMode; private bool _is3DScaleMode;
private GameObject[] _gameObjects;
private static readonly HashSet<Shader> s_Shaders = new HashSet<Shader>(); private static readonly HashSet<Shader> s_Shaders = new HashSet<Shader>();
#if UNITY_2018 || UNITY_2019 #if UNITY_2018 || UNITY_2019
@@ -183,6 +185,13 @@ namespace Coffee.UIExtensions
y.hasMultipleDifferentValues || y.hasMultipleDifferentValues ||
z.hasMultipleDifferentValues; z.hasMultipleDifferentValues;
} }
// Add temporary ParticleSystem for preview if enabled.
if (UIParticleProjectSettings.s_PreviewOnSelectOnSelect)
{
_gameObjects = targets.OfType<UIParticle>().Select(x => x.gameObject).ToArray();
ParticleSystemPreviewSystem.Register(_gameObjects);
}
} }
/// <summary> /// <summary>
@@ -197,7 +206,10 @@ namespace Coffee.UIExtensions
serializedObject.Update(); serializedObject.Update();
// Maskable // Maskable
if (_maskable != null)
{
EditorGUILayout.PropertyField(_maskable); EditorGUILayout.PropertyField(_maskable);
}
// Scale // Scale
EditorGUI.BeginDisabledGroup(!_meshSharing.hasMultipleDifferentValues && _meshSharing.intValue == 4); EditorGUI.BeginDisabledGroup(!_meshSharing.hasMultipleDifferentValues && _meshSharing.intValue == 4);
@@ -344,6 +356,10 @@ namespace Coffee.UIExtensions
} }
} }
#endif #endif
// Remove the temporary ParticleSystem.
ParticleSystemPreviewSystem.DrawWarningForTemporary(_gameObjects);
Profiler.EndSample(); Profiler.EndSample();
} }
@@ -482,7 +498,7 @@ namespace Coffee.UIExtensions
#endif #endif
} }
private static bool FixButton(bool show, string text) private static bool FixButton(bool show, string text, GUIContent buttonText = null)
{ {
if (!show) return false; if (!show) return false;
using (new EditorGUILayout.HorizontalScope(GUILayout.ExpandWidth(true))) using (new EditorGUILayout.HorizontalScope(GUILayout.ExpandWidth(true)))
@@ -490,7 +506,7 @@ namespace Coffee.UIExtensions
EditorGUILayout.HelpBox(text, MessageType.Warning, true); EditorGUILayout.HelpBox(text, MessageType.Warning, true);
using (new EditorGUILayout.VerticalScope()) using (new EditorGUILayout.VerticalScope())
{ {
return GUILayout.Button(s_ContentFix, GUILayout.Width(30)); return GUILayout.Button(buttonText ?? s_ContentFix, GUILayout.ExpandWidth(false));
} }
} }
} }

View File

@@ -6,11 +6,22 @@ namespace Coffee.UIExtensions
{ {
internal class UIParticleMenu internal class UIParticleMenu
{ {
[MenuItem("GameObject/UI/Particle System (Empty)", false, 2018)] #if UNITY_6000_5_OR_NEWER
private const string k_MenuPathToCreateParticleSystem = "GameObject/Visual Effects/Particle System";
#else
private const string k_MenuPathToCreateParticleSystem = "GameObject/Effects/Particle System";
#endif
#if UNITY_6000_3_OR_NEWER
private const string k_MenuPathForUgui = "GameObject/UI (Canvas)";
#else
private const string k_MenuPathForUgui = "GameObject/UI";
#endif
[MenuItem(k_MenuPathForUgui + "/Particle System (Empty)", false, 2018)]
private static void AddParticleEmpty(MenuCommand menuCommand) private static void AddParticleEmpty(MenuCommand menuCommand)
{ {
// Create empty UI element. // Create empty UI element.
EditorApplication.ExecuteMenuItem("GameObject/UI/Image"); EditorApplication.ExecuteMenuItem(k_MenuPathForUgui + "/Image");
var ui = Selection.activeGameObject; var ui = Selection.activeGameObject;
Object.DestroyImmediate(ui.GetComponent<Image>()); Object.DestroyImmediate(ui.GetComponent<Image>());
@@ -21,7 +32,7 @@ namespace Coffee.UIExtensions
uiParticle.rectTransform.sizeDelta = Vector2.zero; uiParticle.rectTransform.sizeDelta = Vector2.zero;
} }
[MenuItem("GameObject/UI/Particle System", false, 2019)] [MenuItem(k_MenuPathForUgui + "/Particle System", false, 2019)]
private static void AddParticle(MenuCommand menuCommand) private static void AddParticle(MenuCommand menuCommand)
{ {
// Create empty UIEffect. // Create empty UIEffect.
@@ -29,7 +40,7 @@ namespace Coffee.UIExtensions
var uiParticle = Selection.activeGameObject.GetComponent<UIParticle>(); var uiParticle = Selection.activeGameObject.GetComponent<UIParticle>();
// Create ParticleSystem. // Create ParticleSystem.
EditorApplication.ExecuteMenuItem("GameObject/Effects/Particle System"); EditorApplication.ExecuteMenuItem(k_MenuPathToCreateParticleSystem);
var ps = Selection.activeGameObject; var ps = Selection.activeGameObject;
ps.transform.SetParent(uiParticle.transform, false); ps.transform.SetParent(uiParticle.transform, false);
ps.transform.localPosition = Vector3.zero; ps.transform.localPosition = Vector3.zero;

View File

@@ -183,9 +183,14 @@ _This package requires **Unity 2018.3 or later**._
- **Time Scale Multiplier:** Time scale multiplier. - **Time Scale Multiplier:** Time scale multiplier.
- **Rendering Order**: The ParticleSystem list to be rendered. You can change the order and the materials. - **Rendering Order**: The ParticleSystem list to be rendered. You can change the order and the materials.
**NOTE:** Press the `Refresh` button to reconstruct the rendering order based on children ParticleSystem's sorting order > [!TIPS]
> Press the `Refresh` button to reconstruct the rendering order based on children ParticleSystem's sorting order
and z-position. and z-position.
> [!TIPS]
> When a `GameObject` with this component is selected in the editor, a temporary `ParticleSystem` is added if needed so you can preview the effect in the Scene view.
> The generated `ParticleSystem` is marked with `HideFlags.DontSave`, so it is neither saved nor included in builds.
<br><br> <br><br>
### Basic Usage ### Basic Usage
@@ -195,6 +200,10 @@ and z-position.
2. Adjust the ParticleSystem as you like. 2. Adjust the ParticleSystem as you like.
![particle1](https://user-images.githubusercontent.com/12690315/95007359-ca385200-0649-11eb-8383-627c9750bda8.png) ![particle1](https://user-images.githubusercontent.com/12690315/95007359-ca385200-0649-11eb-8383-627c9750bda8.png)
> [!Tips]
> Adding a `UIParticle` to the parent is the recommended setup rather than attaching it directly to the `ParticleSystem`.
> When using `ParticleSystem.emission.rateOverDistance`, it is recommended to move the transform of `UIParticle` rather than the `ParticleSystem`.
<br> <br>
### Usage with Your Existing ParticleSystem Prefab ### Usage with Your Existing ParticleSystem Prefab
@@ -256,13 +265,34 @@ uiParticle.Stop();
- **Unscaled Time:** Update with unscaled delta time. - **Unscaled Time:** Update with unscaled delta time.
- **OnAttracted**: An event called when attracting is complete (per particle). - **OnAttracted**: An event called when attracting is complete (per particle).
<br><br>
### Component: ParticleSystemPreviewer
`ParticleSystemPreviewer` is used to preview a ParticleSystem in the editor.
- When a `GameObject` with this component is selected in the editor, a temporary `ParticleSystem` is added if needed so you can preview the effect in the Scene view.
- The generated `ParticleSystem` is marked with `HideFlags.DontSave`, so it is neither saved nor included in builds.
<br><br> <br><br>
### Project Settings ### Project Settings
![](https://github.com/user-attachments/assets/befc7f34-fb47-4006-831a-eba79fda11ca) You can adjust the project-wide settings for `UI Particle`. (`Edit > Project Settings > UI > UI Particle`)
- Click `Edit > Project Settings` to open the Project Settings window and then select `UI > UI Particle` category. ![](https://github.com/mob-sakai/mob-sakai/releases/download/docs/1782270746550.png)
#### Settings
- **Enable Linear To Gamma**: Automatically correct the color space of the mesh.
#### Editor
- **Hide Generated Component**: Automatically hide the generated `UIParticleRenderer` component and `UIParticle BakingCamera`.
- **Preview On Select**: When selecting UIParticle, a temporary ParticleSystem is generated for preview.
<br><br> <br><br>

View File

@@ -37,37 +37,17 @@ namespace Coffee.UIExtensions
switch (type) switch (type)
{ {
case ShaderPropertyType.Color: case ShaderPropertyType.Color:
var color = mpb.GetColor(id); material.SetColor(id, mpb.GetColor(id));
if (color != default)
{
material.SetColor(id, color);
}
break; break;
case ShaderPropertyType.Vector: case ShaderPropertyType.Vector:
var vector = mpb.GetVector(id); material.SetVector(id, mpb.GetVector(id));
if (vector != default)
{
material.SetVector(id, vector);
}
break; break;
case ShaderPropertyType.Float: case ShaderPropertyType.Float:
case ShaderPropertyType.Range: case ShaderPropertyType.Range:
var value = mpb.GetFloat(id); material.SetFloat(id, mpb.GetFloat(id));
if (!Mathf.Approximately(value, 0))
{
material.SetFloat(id, value);
}
break; break;
case ShaderPropertyType.Texture: case ShaderPropertyType.Texture:
var tex = mpb.GetTexture(id); material.SetTexture(id, mpb.GetTexture(id));
if (tex != default(Texture))
{
material.SetTexture(id, tex);
}
break; break;
} }
} }

View File

@@ -12,6 +12,33 @@ namespace Coffee.UIParticleInternal
/// </summary> /// </summary>
internal static class ComponentExtensions internal static class ComponentExtensions
{ {
#if !UNITY_2019_2_OR_NEWER
public static bool TryGetComponent<T>(this GameObject self, out T component)
where T : Component
{
if (self == null)
{
component = null;
return false;
}
component = self.GetComponent<T>();
return component != null;
}
public static bool TryGetComponent<T>(this Component self, out T component)
where T : Component
{
if (self == null)
{
component = null;
return false;
}
return self.gameObject.TryGetComponent(out component);
}
#endif
/// <summary> /// <summary>
/// Get components in children of a specific type in the hierarchy of a GameObject. /// Get components in children of a specific type in the hierarchy of a GameObject.
/// </summary> /// </summary>

View File

@@ -14,6 +14,15 @@ namespace Coffee.UIParticleInternal
public abstract class PreloadedProjectSettings : ScriptableObject public abstract class PreloadedProjectSettings : ScriptableObject
#if UNITY_EDITOR #if UNITY_EDITOR
{ {
[Tooltip("When enabled, this settings asset will be added to PlayerSettings.preloadedAssets in build.\n\n" +
"When disable, you should load this settings via Resources, AssetBundles or Addressables to use.")]
[SerializeField]
[Header("Advanced")]
[HideInInspector]
private bool m_PreLoadSettingsInBuild = true;
protected static bool s_BuildingPlayer;
private class Postprocessor : AssetPostprocessor private class Postprocessor : AssetPostprocessor
{ {
private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___, string[] ____) private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___, string[] ____)
@@ -22,12 +31,35 @@ namespace Coffee.UIParticleInternal
} }
} }
private class PreprocessBuildWithReport : IPreprocessBuildWithReport private class ExcludeFromBuild : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{ {
int IOrderedCallback.callbackOrder => 0; int IOrderedCallback.callbackOrder => 0;
void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report) void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report)
{ {
AssetDatabase.Refresh();
Initialize();
s_BuildingPlayer = true;
foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>)))
{
var settings = GetDefaultSettings(t);
if (!settings || settings.m_PreLoadSettingsInBuild) continue;
PlayerSettings.SetPreloadedAssets(
PlayerSettings.GetPreloadedAssets()
.Where(x => x && x.GetType() != t)
.ToArray());
Debug.Log($"[PreloadedProjectSettings] Build started: removed '{settings.name}' " +
$"({t.Name}) from PreloadedAssets. " +
$"It will be restored after build completes.");
}
}
void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report)
{
s_BuildingPlayer = false;
Initialize(); Initialize();
} }
} }
@@ -38,14 +70,17 @@ namespace Coffee.UIParticleInternal
{ {
var defaultSettings = GetDefaultSettings(t); var defaultSettings = GetDefaultSettings(t);
if (defaultSettings == null) if (defaultSettings == null)
{
if (!s_BuildingPlayer)
{ {
// When create a new instance, automatically set it as default settings. // When create a new instance, automatically set it as default settings.
defaultSettings = CreateInstance(t) as PreloadedProjectSettings; defaultSettings = CreateInstance(t) as PreloadedProjectSettings;
SetDefaultSettings(defaultSettings); SetDefaultSettings(defaultSettings);
} }
}
else if (GetPreloadedSettings(t).Length != 1) else if (GetPreloadedSettings(t).Length != 1)
{ {
SetDefaultSettings(defaultSettings); if (!s_BuildingPlayer) SetDefaultSettings(defaultSettings);
} }
if (defaultSettings != null) if (defaultSettings != null)
@@ -73,7 +108,7 @@ namespace Coffee.UIParticleInternal
protected static PreloadedProjectSettings GetDefaultSettings(Type type) protected static PreloadedProjectSettings GetDefaultSettings(Type type)
{ {
return GetPreloadedSettings(type).FirstOrDefault() as PreloadedProjectSettings return GetPreloadedSettings(type).FirstOrDefault() as PreloadedProjectSettings
?? AssetDatabase.FindAssets($"t:{nameof(PreloadedProjectSettings)}") ?? AssetDatabase.FindAssets($"t:{type.Name}")
.Select(AssetDatabase.GUIDToAssetPath) .Select(AssetDatabase.GUIDToAssetPath)
.Select(AssetDatabase.LoadAssetAtPath<PreloadedProjectSettings>) .Select(AssetDatabase.LoadAssetAtPath<PreloadedProjectSettings>)
.FirstOrDefault(x => x != null && x.GetType() == type); .FirstOrDefault(x => x != null && x.GetType() == type);
@@ -120,6 +155,35 @@ namespace Coffee.UIParticleInternal
{ {
} }
} }
internal abstract class PreloadedProjectSettingsEditor : Editor
{
private SerializedProperty _preLoadSettingsInBuild;
protected virtual void OnEnable()
{
_preLoadSettingsInBuild = serializedObject.FindProperty("m_PreLoadSettingsInBuild");
}
protected void DrawPreLoadSettingsInBuild(string packageName)
{
EditorGUILayout.PropertyField(_preLoadSettingsInBuild);
if (!_preLoadSettingsInBuild.boolValue)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.HelpBox(
$"{target.GetType().Name} asset will not be built in.\n" +
$"please load manually from Resources, AssetBundle, or Addressables before using {packageName}.",
MessageType.Warning);
if (GUILayout.Button("Ping"))
{
EditorGUIUtility.PingObject(target);
}
EditorGUILayout.EndHorizontal();
}
}
}
#else #else
{ {
} }
@@ -151,13 +215,15 @@ namespace Coffee.UIParticleInternal
return s_Instance; return s_Instance;
} }
SetDefaultSettings(s_Instance); if (!s_BuildingPlayer) SetDefaultSettings(s_Instance);
return s_Instance; return s_Instance;
} }
} }
private void OnPlayModeStateChanged(PlayModeStateChange state) private void OnPlayModeStateChanged(PlayModeStateChange state)
{ {
if (!this) return;
switch (state) switch (state)
{ {
case PlayModeStateChange.ExitingEditMode: case PlayModeStateChange.ExitingEditMode:
@@ -191,9 +257,12 @@ namespace Coffee.UIParticleInternal
} }
EditorApplication.playModeStateChanged += OnPlayModeStateChanged; EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#else
if (s_Instance && s_Instance != this)
{
Destroy(s_Instance);
}
#endif #endif
if (s_Instance != null) return;
s_Instance = this as T; s_Instance = this as T;
} }

View File

@@ -14,7 +14,7 @@ namespace Coffee.UIParticleInternal
UIExtraCallbacks.onLateAfterCanvasRebuild += ClearAllCache; UIExtraCallbacks.onLateAfterCanvasRebuild += ClearAllCache;
} }
#if UNITY_EDITOR #if UNITY_EDITOR && UNITY_2019_2_OR_NEWER
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void Clear() private static void Clear()
{ {

View File

@@ -11,7 +11,7 @@ using Conditional = System.Diagnostics.ConditionalAttribute;
namespace Coffee.UIParticleInternal namespace Coffee.UIParticleInternal
{ {
internal static class Logging internal static class Logger
{ {
#if !ENABLE_COFFEE_LOGGER #if !ENABLE_COFFEE_LOGGER
private const string k_DisableSymbol = "DISABLE_COFFEE_LOGGER"; private const string k_DisableSymbol = "DISABLE_COFFEE_LOGGER";

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 8255313895da84e7cbdc876be3795334 guid: 4f9f22bb079324476b1473030ad9fec3
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -13,7 +13,9 @@ namespace Coffee.UIParticleInternal
public static int count => s_Repository.count; public static int count => s_Repository.count;
#if UNITY_EDITOR public static Func<string, Shader> onShaderFind = Shader.Find;
#if UNITY_EDITOR && UNITY_2019_2_OR_NEWER
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
public static void Clear() public static void Clear()
{ {
@@ -48,7 +50,7 @@ namespace Coffee.UIParticleInternal
public static void Get(Hash128 hash, ref Material material, string shaderName) public static void Get(Hash128 hash, ref Material material, string shaderName)
{ {
Profiler.BeginSample("(COF)[MaterialRepository] Get"); Profiler.BeginSample("(COF)[MaterialRepository] Get");
s_Repository.Get(hash, ref material, x => new Material(Shader.Find(x)) s_Repository.Get(hash, ref material, x => new Material(onShaderFind(x))
{ {
hideFlags = HideFlags.DontSave | HideFlags.NotEditable hideFlags = HideFlags.DontSave | HideFlags.NotEditable
}, shaderName); }, shaderName);
@@ -61,7 +63,7 @@ namespace Coffee.UIParticleInternal
public static void Get(Hash128 hash, ref Material material, string shaderName, string[] keywords) public static void Get(Hash128 hash, ref Material material, string shaderName, string[] keywords)
{ {
Profiler.BeginSample("(COF)[MaterialRepository] Get"); Profiler.BeginSample("(COF)[MaterialRepository] Get");
s_Repository.Get(hash, ref material, x => new Material(Shader.Find(x.shaderName)) s_Repository.Get(hash, ref material, x => new Material(onShaderFind(x.shaderName))
{ {
hideFlags = HideFlags.DontSave | HideFlags.NotEditable, hideFlags = HideFlags.DontSave | HideFlags.NotEditable,
shaderKeywords = x.keywords shaderKeywords = x.keywords

View File

@@ -20,7 +20,9 @@ namespace Coffee.UIParticleInternal
{ {
public static T[] FindObjectsOfType<T>() where T : Object public static T[] FindObjectsOfType<T>() where T : Object
{ {
#if UNITY_2023_1_OR_NEWER #if UNITY_6000_4_OR_NEWER
return Object.FindObjectsByType<T>(FindObjectsInactive.Include);
#elif UNITY_2023_1_OR_NEWER
return Object.FindObjectsByType<T>(FindObjectsInactive.Include, FindObjectsSortMode.None); return Object.FindObjectsByType<T>(FindObjectsInactive.Include, FindObjectsSortMode.None);
#else #else
return Object.FindObjectsOfType<T>(); return Object.FindObjectsOfType<T>();
@@ -46,15 +48,10 @@ namespace Coffee.UIParticleInternal
{ {
if (obj == null) return; if (obj == null) return;
#if UNITY_EDITOR #if UNITY_EDITOR
if (Application.isEditor) Object.DestroyImmediate(obj, true);
{ #else
Object.DestroyImmediate(obj);
}
else
#endif
{
Object.Destroy(obj); Object.Destroy(obj);
} #endif
} }
[Conditional("UNITY_EDITOR")] [Conditional("UNITY_EDITOR")]
@@ -112,7 +109,7 @@ namespace Coffee.UIParticleInternal
{ {
if (Misc.isBatchOrBuilding) return; if (Misc.isBatchOrBuilding) return;
var types = TypeCache.GetTypesWithAttribute<IconAttribute>(); var types = TypeCache.GetTypesWithAttribute(typeof(IconAttribute));
var scripts = MonoImporter.GetAllRuntimeMonoScripts(); var scripts = MonoImporter.GetAllRuntimeMonoScripts();
foreach (var type in types) foreach (var type in types)
{ {

View File

@@ -34,7 +34,7 @@ namespace Coffee.UIParticleInternal
} }
// If there are no instances in the pool, create a new one. // If there are no instances in the pool, create a new one.
Logging.Log(this, $"A new instance is created (pooled: {_pool.CountInactive}, created: {_pool.CountAll})."); Logger.Log(this, $"A new instance is created (pooled: {_pool.CountInactive}, created: {_pool.CountAll}).");
return _pool.Get(); return _pool.Get();
} }
@@ -47,7 +47,7 @@ namespace Coffee.UIParticleInternal
if (instance == null) return; // Ignore if already pooled or null. if (instance == null) return; // Ignore if already pooled or null.
_pool.Release(instance); _pool.Release(instance);
Logging.Log(this, $"An instance is released (pooled: {_pool.CountInactive}, created: {_pool.CountAll})."); Logger.Log(this, $"An instance is released (pooled: {_pool.CountInactive}, created: {_pool.CountAll}).");
instance = default; // Set the reference to null. instance = default; // Set the reference to null.
} }
#else #else
@@ -80,7 +80,7 @@ namespace Coffee.UIParticleInternal
} }
// If there are no instances in the pool, create a new one. // If there are no instances in the pool, create a new one.
Logging.Log(this, $"A new instance is created (pooled: {_pool.Count}, created: {++_count})."); Logger.Log(this, $"A new instance is created (pooled: {_pool.Count}, created: {++_count}).");
return _onCreate(); return _onCreate();
} }
@@ -94,7 +94,7 @@ namespace Coffee.UIParticleInternal
_onReturn(instance); // Return the instance to the pool. _onReturn(instance); // Return the instance to the pool.
_pool.Push(instance); _pool.Push(instance);
Logging.Log(this, $"An instance is released (pooled: {_pool.Count}, created: {_count})."); Logger.Log(this, $"An instance is released (pooled: {_pool.Count}, created: {_count}).");
instance = default; // Set the reference to null. instance = default; // Set the reference to null.
} }
#endif #endif

View File

@@ -103,7 +103,7 @@ namespace Coffee.UIParticleInternal
Release(ref obj); Release(ref obj);
++entry.reference; ++entry.reference;
obj = entry.storedObject; obj = entry.storedObject;
Logging.Log(_name, $"Get(total#{count}): {entry}"); Logger.Log(_name, $"Get(total#{count}): {entry}");
} }
Profiler.EndSample(); Profiler.EndSample();
@@ -130,8 +130,8 @@ namespace Coffee.UIParticleInternal
newEntry.hash = hash; newEntry.hash = hash;
newEntry.reference = 1; newEntry.reference = 1;
_cache[hash] = newEntry; _cache[hash] = newEntry;
_objectKey[newObject.GetInstanceID()] = hash; _objectKey[newObject.GetHashCode()] = hash;
Logging.Log(_name, $"<color=#03c700>Add</color>(total#{count}): {newEntry}"); Logger.Log(_name, $"<color=#03c700>Add</color>(total#{count}): {newEntry}");
Release(ref obj); Release(ref obj);
obj = newObject; obj = newObject;
Profiler.EndSample(); Profiler.EndSample();
@@ -146,7 +146,7 @@ namespace Coffee.UIParticleInternal
// Find and release the entry. // Find and release the entry.
Profiler.BeginSample("(COF)[ObjectRepository] Release"); Profiler.BeginSample("(COF)[ObjectRepository] Release");
var id = obj.GetInstanceID(); var id = obj.GetHashCode();
if (_objectKey.TryGetValue(id, out var hash) if (_objectKey.TryGetValue(id, out var hash)
&& _cache.TryGetValue(hash, out var entry)) && _cache.TryGetValue(hash, out var entry))
{ {
@@ -157,12 +157,12 @@ namespace Coffee.UIParticleInternal
} }
else else
{ {
Logging.Log(_name, $"Release(total#{_cache.Count}): {entry}"); Logger.Log(_name, $"Release(total#{_cache.Count}): {entry}");
} }
} }
else else
{ {
Logging.Log(_name, $"Release(total#{_cache.Count}): <color=red>Already released: {obj}</color>"); Logger.Log(_name, $"Release(total#{_cache.Count}): <color=red>Already released: {obj}</color>");
} }
obj = null; obj = null;
@@ -175,10 +175,10 @@ namespace Coffee.UIParticleInternal
Profiler.BeginSample("(COF)[ObjectRepository] Remove"); Profiler.BeginSample("(COF)[ObjectRepository] Remove");
_cache.Remove(entry.hash); _cache.Remove(entry.hash);
_objectKey.Remove(entry.storedObject.GetInstanceID()); _objectKey.Remove(entry.storedObject.GetHashCode());
_pool.Push(entry); _pool.Push(entry);
entry.reference = 0; entry.reference = 0;
Logging.Log(_name, $"<color=#f29e03>Remove</color>(total#{_cache.Count}): {entry}"); Logger.Log(_name, $"<color=#f29e03>Remove</color>(total#{_cache.Count}): {entry}");
entry.Release(_onRelease); entry.Release(_onRelease);
Profiler.EndSample(); Profiler.EndSample();
} }

View File

@@ -0,0 +1,88 @@
#if !UNITY_2019_2_OR_NEWER
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Coffee.UIParticleInternal
{
public static class TypeCache
{
private static readonly object s_Lock = new object();
private static readonly Dictionary<Type, Type[]> s_DerivedTypesCache = new Dictionary<Type, Type[]>();
private static readonly Dictionary<Type, Type[]> s_AttributeTypesCache = new Dictionary<Type, Type[]>();
public static IEnumerable<Type> GetTypesDerivedFrom(Type baseType)
{
lock (s_Lock)
{
if (s_DerivedTypesCache.TryGetValue(baseType, out var cached))
{
return cached;
}
var types = new List<Type>();
foreach (var t in GetAllLoadableTypes())
{
if (t != baseType && baseType.IsAssignableFrom(t))
{
types.Add(t);
}
}
return s_DerivedTypesCache[baseType] = types.ToArray();
}
}
public static IEnumerable<Type> GetTypesWithAttribute(Type attr)
{
lock (s_Lock)
{
if (s_AttributeTypesCache.TryGetValue(attr, out var cached))
{
return cached;
}
var types = new List<Type>();
foreach (var t in GetAllLoadableTypes())
{
if (t.GetCustomAttributes(attr, inherit: true).Length > 0)
{
types.Add(t);
}
}
return s_AttributeTypesCache[attr] = types.ToArray();
}
}
private static IEnumerable<Type> GetAllLoadableTypes()
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types;
}
if (types == null)
{
continue;
}
foreach (var t in types)
{
if (t != null)
{
yield return t;
}
}
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc1207b657ed74ec19e459664d06925f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@@ -20,7 +21,7 @@ namespace Coffee.UIParticleInternal
static UIExtraCallbacks() static UIExtraCallbacks()
{ {
Canvas.willRenderCanvases += OnBeforeCanvasRebuild; Canvas.willRenderCanvases += OnBeforeCanvasRebuild;
Logging.LogMulticast(typeof(Canvas), "willRenderCanvases", message: "ctor"); Logger.LogMulticast(typeof(Canvas), "willRenderCanvases", message: "ctor");
} }
/// <summary> /// <summary>
@@ -67,9 +68,17 @@ namespace Coffee.UIParticleInternal
if (s_IsInitializedAfterCanvasRebuild) return; if (s_IsInitializedAfterCanvasRebuild) return;
s_IsInitializedAfterCanvasRebuild = true; s_IsInitializedAfterCanvasRebuild = true;
// Explicitly set `Canvas.willRenderCanvases += CanvasUpdateRegistry.PerformUpdate`.
CanvasUpdateRegistry.IsRebuildingLayout(); CanvasUpdateRegistry.IsRebuildingLayout();
#if TMP_ENABLE
// Explicitly set `Canvas.willRenderCanvases += TMP_UpdateManager.DoRebuilds`.
typeof(TMPro.TMP_UpdateManager)
.GetProperty("instance", BindingFlags.NonPublic | BindingFlags.Static)
.GetValue(null);
#endif
Canvas.willRenderCanvases -= OnAfterCanvasRebuild;
Canvas.willRenderCanvases += OnAfterCanvasRebuild; Canvas.willRenderCanvases += OnAfterCanvasRebuild;
Logging.LogMulticast(typeof(Canvas), "willRenderCanvases", Logger.LogMulticast(typeof(Canvas), "willRenderCanvases",
message: "InitializeAfterCanvasRebuild"); message: "InitializeAfterCanvasRebuild");
} }

View File

@@ -0,0 +1,222 @@
using System.Collections.Generic;
using System.Linq;
using Coffee.UIParticleInternal;
using UnityEditor;
using UnityEngine;
namespace Coffee.UIExtensions
{
[Icon("Packages/com.coffee.ui-particle/Editor/UIParticleIcon.png")]
[ExecuteAlways]
internal class ParticleSystemPreviewer : MonoBehaviour
{
// Do nothing.
}
#if UNITY_EDITOR
[CustomEditor(typeof(ParticleSystemPreviewer))]
[CanEditMultipleObjects]
internal class ParticleSystemPreviewerEditor : Editor
{
private GameObject[] _gameObjects;
private void OnEnable()
{
_gameObjects = targets.OfType<ParticleSystemPreviewer>().Select(x => x.gameObject).ToArray();
ParticleSystemPreviewSystem.Register(_gameObjects);
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
ParticleSystemPreviewSystem.DrawWarningForTemporary(_gameObjects);
ParticleSystemPreviewSystem.DrawWarningForPermanent(_gameObjects);
}
}
/// <summary>
/// This class manages temporary ParticleSystems for preview purposes.
/// When previewing in the editor, it is common to place an empty ParticleSystem as the root, but it consumes memory at runtime if included in the build.
/// The temporary ParticleSystems created by this class only exist when the specified GameObject is selected, and are automatically deleted when the selection is cleared.
/// </summary>
internal class ParticleSystemPreviewSystem : ScriptableSingleton<ParticleSystemPreviewSystem>
{
private const HideFlags k_TemporaryHideFlags = HideFlags.DontSave | HideFlags.NotEditable;
[SerializeField]
private List<GameObject> m_PreviewObjects = new List<GameObject>();
/// <summary>
/// Adds a temporary ParticleSystem to the specified GameObject for preview purposes.
/// </summary>
public static void Register(GameObject[] targets)
{
foreach (var target in targets)
{
Register(target);
}
}
/// <summary>
/// Adds a temporary ParticleSystem to the specified GameObject for preview purposes.
/// </summary>
public static void Register(GameObject target)
{
if (!target) return;
if (EditorApplication.isPlaying) return;
if (instance.m_PreviewObjects.Contains(target)) return;
if (target.TryGetComponent<ParticleSystem>(out var _)) return;
// Create temporary ParticleSystem for preview.
var ps = target.AddComponent<ParticleSystem>();
ps.hideFlags = k_TemporaryHideFlags;
var emission = ps.emission;
emission.enabled = false;
var shape = ps.shape;
shape.enabled = false;
if (target.TryGetComponent<ParticleSystemRenderer>(out var psr))
{
psr.enabled = false;
psr.hideFlags = k_TemporaryHideFlags;
}
instance.m_PreviewObjects.Add(target);
EditorUtility.SetDirty(target);
}
/// <summary>
/// Removes the temporary ParticleSystem associated with the specified GameObject.
/// </summary>
/// <param name="target"></param>
public static void Unregister(GameObject target)
{
if (!target) return;
var index = instance.m_PreviewObjects.IndexOf(target);
if (index < 0) return;
instance.m_PreviewObjects.RemoveAt(index);
if (HasTemporaryParticleSystem(target))
{
RemoveParticleSystem(target);
}
}
/// <summary>
/// Removes the temporary ParticleSystem associated with the specified GameObject.
/// </summary>
/// <param name="target"></param>
private static void RemoveParticleSystem(GameObject target)
{
if (target.TryGetComponent<ParticleSystem>(out var ps))
{
Misc.DestroyImmediate(ps);
EditorUtility.SetDirty(target);
}
if (target.TryGetComponent<ParticleSystem>(out var psr))
{
Misc.DestroyImmediate(psr);
EditorUtility.SetDirty(target);
}
}
/// <summary>
/// Checks if the specified GameObject has a temporary ParticleSystem.
/// </summary>
private static bool HasTemporaryParticleSystem(GameObject target)
{
return target
&& instance.m_PreviewObjects.Contains(target)
&& target.TryGetComponent<ParticleSystem>(out var ps)
&& ps.hideFlags == k_TemporaryHideFlags;
}
/// <summary>
/// Checks if the specified GameObject has a permanent ParticleSystem.
/// </summary>
private static bool HasPermanentParticleSystem(GameObject target)
{
return target
&& target.TryGetComponent<ParticleSystem>(out var ps)
&& ps.hideFlags != k_TemporaryHideFlags;
}
private void OnEnable()
{
Selection.selectionChanged -= OnSelectionChanged;
Selection.selectionChanged += OnSelectionChanged;
}
private void OnDisable()
{
Selection.selectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged()
{
var selectedGameObjects = Selection.gameObjects;
for (var i = m_PreviewObjects.Count - 1; 0 <= i; i--)
{
var go = m_PreviewObjects[i];
if (!go)
{
m_PreviewObjects.RemoveAt(i);
}
else if (!selectedGameObjects.Contains(go))
{
Unregister(go);
}
}
}
public static void DrawWarningForTemporary(GameObject[] gameObjects)
{
if (gameObjects == null || gameObjects.Length == 0 || !gameObjects.Any(HasTemporaryParticleSystem)) return;
if (WarningButton("The temporary ParticleSystem for preview is attached.\n" +
"It will be removed when exiting edit mode.", "Remove"))
{
foreach (var go in gameObjects)
{
if (HasTemporaryParticleSystem(go))
{
RemoveParticleSystem(go);
}
}
}
}
public static void DrawWarningForPermanent(GameObject[] gameObjects)
{
if (gameObjects == null || gameObjects.Length == 0 || !gameObjects.Any(HasPermanentParticleSystem)) return;
if (WarningButton("The permanent ParticleSystem is attached.\n" +
"It will be included in build.", "Remove"))
{
foreach (var go in gameObjects)
{
if (HasPermanentParticleSystem(go))
{
RemoveParticleSystem(go);
Unregister(go);
Register(go);
}
}
}
}
private static bool WarningButton(string message, string buttonText)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.HelpBox(message, MessageType.Warning, true);
var clicked = GUILayout.Button(EditorGUIUtility.TrTempContent(buttonText));
EditorGUILayout.EndHorizontal();
return clicked;
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b171deb49fb7b471291108ad7e1c9baa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -358,6 +358,7 @@ namespace Coffee.UIExtensions
_isScaleStored = false; _isScaleStored = false;
UIParticleUpdater.Unregister(this); UIParticleUpdater.Unregister(this);
_renderers.RemoveAll(r => r == null);
_renderers.ForEach(r => r.Reset()); _renderers.ForEach(r => r.Reset());
UnregisterDirtyMaterialCallback(UpdateRendererMaterial); UnregisterDirtyMaterialCallback(UpdateRendererMaterial);
@@ -543,7 +544,12 @@ namespace Coffee.UIExtensions
for (var i = particles.Count - 1; 0 <= i; i--) for (var i = particles.Count - 1; 0 <= i; i--)
{ {
var ps = particles[i]; var ps = particles[i];
if (!ps || ps.GetComponentInParent<UIParticle>(true) != this) if (!ps
#if UNITY_EDITOR
|| ps.hideFlags == HideFlags.HideAndDontSave // Dummy ParticleSystems for preview.
|| ps.gameObject.CompareTag("EditorOnly") // Ignore "EditorOnly" tagged ParticleSystems.
#endif
|| ps.GetComponentInParent<UIParticle>(true) != this) // Ignore ParticleSystems that are not under this UIParticle.
{ {
particles.RemoveAt(i); particles.RemoveAt(i);
} }

View File

@@ -2,6 +2,7 @@
using Coffee.UIParticleInternal; using Coffee.UIParticleInternal;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization;
namespace Coffee.UIExtensions namespace Coffee.UIExtensions
{ {
@@ -9,12 +10,14 @@ namespace Coffee.UIExtensions
{ {
[Header("Setting")] [Header("Setting")]
[SerializeField] [SerializeField]
internal bool m_EnableLinearToGamma = true; [Tooltip("Automatically correct the color space of the mesh.")]
[FormerlySerializedAs("m_EnableLinearToGamma")]
internal bool m_AutoColorCorrection = true;
public static bool enableLinearToGamma public static bool autoColorCorrection
{ {
get => instance.m_EnableLinearToGamma; get => instance.m_AutoColorCorrection;
set => instance.m_EnableLinearToGamma = value; set => instance.m_AutoColorCorrection = value;
} }
@@ -25,10 +28,16 @@ namespace Coffee.UIExtensions
[SerializeField] [SerializeField]
private bool m_HideGeneratedObjects = true; private bool m_HideGeneratedObjects = true;
public static HideFlags globalHideFlags => instance.m_HideGeneratedObjects [Tooltip("When selecting UIParticle, a temporary ParticleSystem is generated for preview.")]
[SerializeField]
private bool m_PreviewOnSelect = true;
internal static HideFlags globalHideFlags => instance.m_HideGeneratedObjects
? HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInHierarchy | HideFlags.HideInInspector ? HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInHierarchy | HideFlags.HideInInspector
: HideFlags.DontSave | HideFlags.NotEditable; : HideFlags.DontSave | HideFlags.NotEditable;
internal static bool s_PreviewOnSelectOnSelect => instance.m_PreviewOnSelect;
#if UNITY_EDITOR #if UNITY_EDITOR
[SettingsProvider] [SettingsProvider]
private static SettingsProvider CreateSettingsProvider() private static SettingsProvider CreateSettingsProvider()

View File

@@ -30,6 +30,7 @@ namespace Coffee.UIExtensions
private int _index; private int _index;
private bool _isPrevStored; private bool _isPrevStored;
private bool _isTrail; private bool _isTrail;
private bool _meshCleared;
private Bounds _lastBounds; private Bounds _lastBounds;
private Material _materialForRendering; private Material _materialForRendering;
private Material _modifiedMaterial; private Material _modifiedMaterial;
@@ -205,9 +206,9 @@ namespace Coffee.UIExtensions
} }
var hash = new Hash128( var hash = new Hash128(
modifiedMaterial ? (uint)modifiedMaterial.GetInstanceID() : 0, modifiedMaterial ? (uint)modifiedMaterial.GetHashCode() : 0,
texture ? (uint)texture.GetInstanceID() : 0, texture ? (uint)texture.GetHashCode() : 0,
0 < _parent.m_AnimatableProperties.Length ? (uint)GetInstanceID() : 0, 0 < _parent.m_AnimatableProperties.Length ? (uint)GetHashCode() : 0,
#if UNITY_EDITOR #if UNITY_EDITOR
(uint)EditorJsonUtility.ToJson(modifiedMaterial).GetHashCode() (uint)EditorJsonUtility.ToJson(modifiedMaterial).GetHashCode()
#else #else
@@ -285,10 +286,14 @@ namespace Coffee.UIExtensions
|| (_isTrail && !_particleSystem.trails.enabled) // Trail, but it is not enabled. || (_isTrail && !_particleSystem.trails.enabled) // Trail, but it is not enabled.
) )
{ {
// Skip clearing the mesh if it's already cleared.
if (_meshCleared) return;
Profiler.BeginSample("[UIParticleRenderer] Clear Mesh"); Profiler.BeginSample("[UIParticleRenderer] Clear Mesh");
workerMesh.Clear(); workerMesh.Clear();
canvasRenderer.SetMesh(workerMesh); canvasRenderer.SetMesh(workerMesh);
_lastBounds = new Bounds(); _lastBounds = new Bounds();
_meshCleared = true;
Profiler.EndSample(); Profiler.EndSample();
return; return;
@@ -312,6 +317,7 @@ namespace Coffee.UIExtensions
// customData.SetVector(ParticleSystemCustomData.Custom2, 3, 0); // customData.SetVector(ParticleSystemCustomData.Custom2, 3, 0);
// } // }
_meshCleared = false;
var main = _particleSystem.main; var main = _particleSystem.main;
var scale = GetWorldScale(); var scale = GetWorldScale();
var psPos = _particleSystem.transform.position; var psPos = _particleSystem.transform.position;
@@ -438,7 +444,7 @@ namespace Coffee.UIExtensions
_lastBounds = bounds; _lastBounds = bounds;
// Convert linear color to gamma color. // Convert linear color to gamma color.
if (UIParticleProjectSettings.enableLinearToGamma && canvas.ShouldGammaToLinearInMesh()) if (UIParticleProjectSettings.autoColorCorrection && canvas.ShouldGammaToLinearInMesh())
{ {
workerMesh.LinearToGamma(); workerMesh.LinearToGamma();
} }

View File

@@ -92,7 +92,7 @@ namespace Coffee.UIParticleInternal
if (sortByMaterial) if (sortByMaterial)
{ {
return aMat.GetInstanceID() - bMat.GetInstanceID(); return aMat.GetHashCode() - bMat.GetHashCode();
} }
if (aMat.renderQueue != bMat.renderQueue) if (aMat.renderQueue != bMat.renderQueue)
@@ -131,7 +131,7 @@ namespace Coffee.UIParticleInternal
{ {
for (var i = 0; i < list.Count; i++) for (var i = 0; i < list.Count; i++)
{ {
if (list[i].GetInstanceID() == ps.GetInstanceID()) if (list[i].GetHashCode() == ps.GetHashCode())
{ {
return i; return i;
} }

View File

@@ -40,12 +40,8 @@
Lighting Off Lighting Off
ZWrite Off ZWrite Off
ZTest [unity_GUIZTestMode] ZTest [unity_GUIZTestMode]
Fog Fog { Mode Off }
{
Mode Off
}
Blend One One Blend One One
ColorMask [_ColorMask] ColorMask [_ColorMask]
Pass Pass
@@ -76,6 +72,7 @@
fixed4 color : COLOR; fixed4 color : COLOR;
float2 texcoord : TEXCOORD0; float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1; float4 worldPosition : TEXCOORD1;
float4 mask : TEXCOORD2;
UNITY_VERTEX_OUTPUT_STEREO UNITY_VERTEX_OUTPUT_STEREO
}; };
@@ -84,16 +81,43 @@
float4 _MainTex_ST; float4 _MainTex_ST;
fixed4 _TextureSampleAdd; fixed4 _TextureSampleAdd;
float4 _ClipRect; float4 _ClipRect;
float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
int _UIVertexColorAlwaysGammaSpace;
half3 _UIGammaToLinear(half3 value)
{
half3 low = 0.0849710 * value - 0.000163029;
half3 high = value * (value * (value * 0.265885 + 0.736584) - 0.00980184) + 0.00319697;
// We should be 0.5 away from any actual gamma value stored in an 8 bit channel
const half3 split = (half3)0.0725490; // Equals 18.5 / 255
return (value < split) ? low : high;
}
v2f vert(appdata_t v) v2f vert(appdata_t v)
{ {
v2f OUT; v2f OUT;
UNITY_SETUP_INSTANCE_ID(v); UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float4 vPosition = UnityObjectToClipPos(v.vertex);
OUT.worldPosition = v.vertex; OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); OUT.vertex = vPosition;
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); float2 pixelSize = vPosition.w;
pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));
if (_UIVertexColorAlwaysGammaSpace)
{
#ifndef UNITY_COLORSPACE_GAMMA
v.color.rgb = _UIGammaToLinear(v.color.rgb);
#endif
}
OUT.color = v.color * _Color; OUT.color = v.color * _Color;
return OUT; return OUT;
@@ -101,17 +125,26 @@
fixed4 frag(v2f IN) : SV_Target fixed4 frag(v2f IN) : SV_Target
{ {
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; //Round up the alpha color coming from the interpolator (to 1.0/256.0 steps)
//The incoming alpha could have numerical instability, which makes it very sensible to
//HDR color transparency blend, when it blends with the world's texture.
const half alphaPrecision = half(0xff);
const half invAlphaPrecision = half(1.0 / alphaPrecision);
IN.color.a = round(IN.color.a * alphaPrecision) * invAlphaPrecision;
half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);
#ifdef UNITY_UI_CLIP_RECT #ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
color.a *= m.x * m.y;
#endif #endif
#ifdef UNITY_UI_ALPHACLIP #ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001); clip(color.a - 0.001);
#endif #endif
color.rgb *= color.a; color.rgb *= color.a;
return color; return color;
} }
ENDCG ENDCG

View File

@@ -2,7 +2,7 @@
"name": "com.coffee.ui-particle", "name": "com.coffee.ui-particle",
"displayName": "UI Particle", "displayName": "UI Particle",
"description": "This package provides a component to render particle effects for uGUI.\nThe particle rendering is maskable and sortable, without the need for an extra Camera, RenderTexture, or Canvas.", "description": "This package provides a component to render particle effects for uGUI.\nThe particle rendering is maskable and sortable, without the need for an extra Camera, RenderTexture, or Canvas.",
"version": "4.12.0", "version": "4.12.2",
"unity": "2018.2", "unity": "2018.2",
"license": "MIT", "license": "MIT",
"repository": { "repository": {